Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --warnings-as-errors flag for non-zero exit code (continued) #1564

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/ex_doc/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ defmodule ExDoc.Application do
match?("makeup_" <> _, Atom.to_string(app)),
do: Application.ensure_all_started(app)

Supervisor.start_link([ExDoc.Refs], strategy: :one_for_one)
children = [
ExDoc.Refs,
ExDoc.WarningCounter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of introducing some global state, I wonder if you could create a counter and store it in the config. And then you call warning(config, ...) which will bump the counter. This means it is naturally async and you don't need the additional process. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the same vein, perhaps mix docs would return https://hexdocs.pm/mix/1.15/Mix.Task.Compiler.Diagnostic.html structs? Could be useful for IDEs? Or since ExDoc is not a compiler it’s a bad idea?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't see how they could be used if it is not a compiler :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing config simplifies a lot the implementation.
The problem I am facing now is that there are functions that generate a warning and have no notion of config,
such as ExDoc.Markdown.Earmark.to_ast/2

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to either:

  1. pass an optional function to them to configure how logging happens
  2. have those functions return their warnings so we can emit them

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d do the latter, return {ast, %{warnings: warnings}} or something like that.

I think such APIs that return instead of emit warnings are nice because we can test without capture_io and thus async. In this particular case since it’s not useful to return these warnings from mix docs, what we can also do is push all ExDoc warnings via some ExDoc.Utils.warn, inside call Application.put_env(:ex_doc, :warnings_emitted, true), check for this app env at the end of the run and call it a day. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The counter will tell us if warnings were emitted or not. So having a config plus function return should be enough!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will work on this solution and let you know when it is done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you ❤️

]

Supervisor.start_link(children, strategy: :one_for_one)
end
end
4 changes: 1 addition & 3 deletions lib/ex_doc/autolink.ex
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,12 @@ defmodule ExDoc.Autolink do
end

defp warn(message, {file, line}, id) do
warning = IO.ANSI.format([:yellow, "warning: ", :reset])

stacktrace =
" #{file}" <>
if(line, do: ":#{line}", else: "") <>
if(id, do: ": #{id}", else: "")

IO.puts(:stderr, [warning, message, ?\n, stacktrace, ?\n])
ExDoc.Utils.warning([message, ?\n, stacktrace, ?\n])
end

defp warn(ref, file_line, id, visibility, metadata)
Expand Down
23 changes: 17 additions & 6 deletions lib/ex_doc/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,25 @@ defmodule ExDoc.CLI do
proglang: :string,
quiet: :boolean,
source_ref: :string,
source_url: :string,
version: :boolean
version: :boolean,
warnings_as_errors: :boolean
]
)

if List.keymember?(opts, :version, 0) do
IO.puts("ExDoc v#{ExDoc.version()}")
else
generate(args, opts, generator)
cond do
List.keymember?(opts, :version, 0) ->
IO.puts("ExDoc v#{ExDoc.version()}")

opts[:warnings_as_errors] == true and ExDoc.WarningCounter.count() > 0 ->
IO.puts(
:stderr,
"Doc generation failed due to warnings while using the --warnings-as-errors option"
)

exit({:shutdown, ExDoc.WarningCounter.count()})

true ->
generate(args, opts, generator)
end
end

Expand Down Expand Up @@ -187,6 +197,7 @@ defmodule ExDoc.CLI do
--source-ref Branch/commit/tag used for source link inference, default: "master"
-u, --source-url URL to the source code
-v, --version Print ExDoc version
--warnings-as-errors Exit with non-zero status if doc generation has one or more warnings

## Custom config

Expand Down
6 changes: 4 additions & 2 deletions lib/ex_doc/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ defmodule ExDoc.Config do
source_url: nil,
source_url_pattern: nil,
title: nil,
version: nil
version: nil,
warnings_as_errors: false

@type t :: %__MODULE__{
annotations_for_docs: (map() -> list()),
Expand Down Expand Up @@ -80,7 +81,8 @@ defmodule ExDoc.Config do
source_url: nil | String.t(),
source_url_pattern: nil | String.t(),
title: nil | String.t(),
version: nil | String.t()
version: nil | String.t(),
warnings_as_errors: boolean()
}

@spec build(String.t(), String.t(), Keyword.t()) :: ExDoc.Config.t()
Expand Down
2 changes: 1 addition & 1 deletion lib/ex_doc/formatter/epub.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ defmodule ExDoc.Formatter.EPUB do
html = Templates.extra_template(config, title, title_content, content)

if File.regular?(output) do
IO.puts(:stderr, "warning: file #{Path.relative_to_cwd(output)} already exists")
ExDoc.Utils.warning("file #{Path.relative_to_cwd(output)} already exists")
end

File.write!(output, html)
Expand Down
6 changes: 3 additions & 3 deletions lib/ex_doc/formatter/html.ex
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ defmodule ExDoc.Formatter.HTML do
:ok

true ->
IO.warn(
ExDoc.Utils.warning(
"ExDoc is outputting to an existing directory. " <>
"Beware documentation output may be mixed with other entries"
)
Expand Down Expand Up @@ -263,7 +263,7 @@ defmodule ExDoc.Formatter.HTML do
html = Templates.extra_template(config, node, extra_type(extension), nodes_map, refs)

if File.regular?(output) do
IO.puts(:stderr, "warning: file #{Path.relative_to_cwd(output)} already exists")
ExDoc.Utils.warning("file #{Path.relative_to_cwd(output)} already exists")
end

File.write!(output, html)
Expand Down Expand Up @@ -556,7 +556,7 @@ defmodule ExDoc.Formatter.HTML do

defp generate_redirect(filename, config, redirect_to) do
unless case_sensitive_file_regular?("#{config.output}/#{redirect_to}") do
IO.puts(:stderr, "warning: #{filename} redirects to #{redirect_to}, which does not exist")
ExDoc.Utils.warning("#{filename} redirects to #{redirect_to}, which does not exist")
end

content = Templates.redirect_template(config, redirect_to)
Expand Down
5 changes: 1 addition & 4 deletions lib/ex_doc/language.ex
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,7 @@ defmodule ExDoc.Language do
def get(:erlang, _module), do: {:ok, ExDoc.Language.Erlang}

def get(language, module) when is_atom(language) and is_atom(module) do
IO.warn(
"skipping module #{module}, reason: unsupported language (#{language})",
[]
)
ExDoc.Utils.warning("skipping module #{module}, reason: unsupported language (#{language})")

:error
end
Expand Down
2 changes: 1 addition & 1 deletion lib/ex_doc/language/elixir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule ExDoc.Language.Elixir do
}

true ->
IO.warn("skipping docs for module #{inspect(module)}, reason: :no_debug_info", [])
ExDoc.Utils.warning("skipping docs for module #{inspect(module)}, reason: :no_debug_info")
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/ex_doc/language/erlang.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ defmodule ExDoc.Language.Erlang do
}
}
else
IO.warn("skipping docs for module #{inspect(module)}, reason: :no_debug_info", [])
ExDoc.Utils.warning("skipping docs for module #{inspect(module)}, reason: :no_debug_info")
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/ex_doc/markdown/earmark.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ defmodule ExDoc.Markdown.Earmark do
defp print_messages(messages, options) do
for {severity, line, message} <- messages do
file = options[:file]
IO.warn("#{inspect(__MODULE__)} (#{severity}) #{file}:#{line} #{message}", [])
ExDoc.Utils.warning("#{inspect(__MODULE__)} (#{severity}) #{file}:#{line} #{message}")
end
end

Expand Down
3 changes: 2 additions & 1 deletion lib/ex_doc/retriever.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ defmodule ExDoc.Retriever do
docs

{:error, reason} ->
IO.warn("skipping docs for module #{inspect(module)}, reason: #{reason}", [])
ExDoc.Utils.warning("skipping docs for module #{inspect(module)}, reason: #{reason}")

false
end

Expand Down
13 changes: 13 additions & 0 deletions lib/ex_doc/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,17 @@ defmodule ExDoc.Utils do
nil
end
end

@doc """
Prints `message` as a warning in :stderr.

It automatically increases the counter in `ExDoc.WarningCounter`.
"""
def warning(message) do
ExDoc.WarningCounter.increment()

prefix = IO.ANSI.format([:yellow, "warning: ", :reset])

IO.puts(:stderr, [prefix, message])
end
end
119 changes: 119 additions & 0 deletions lib/ex_doc/warning_counter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
defmodule ExDoc.WarningCounter do
@moduledoc false

use GenServer

@type count :: non_neg_integer()
@type counters_ref :: :counters.counters_ref()
@typep caller :: pid() | :default
@typep state :: %{:default => counters_ref(), caller() => counters_ref()}

defguardp is_caller(term) when is_pid(term) or term == :default

###########################
# Callback implementations

@spec start_link(any()) :: GenServer.on_start()
def start_link(arg) do
GenServer.start_link(__MODULE__, arg, name: __MODULE__)
end

@impl GenServer
@spec init(any()) :: {:ok, state}
def init(_args) do
counter_ref = new_counter()

state = %{
self() => counter_ref,
default: counter_ref
}

{:ok, state}
end

@impl GenServer
def handle_call({:count, caller}, _from, state) when is_caller(caller) do
counter_ref = get_counter_ref(state, caller)
count = :counters.get(counter_ref, 1)

{:reply, count, state}
end

@impl GenServer
def handle_info({:increment, caller}, state) when is_caller(caller) do
counter_ref = get_counter_ref(state, caller)
:counters.add(counter_ref, 1, 1)

{:noreply, state}
end

def handle_info({:register_caller, caller}, state) when is_caller(caller) do
counter_ref = new_counter()
state = Map.put(state, caller, counter_ref)

{:noreply, state}
end

def handle_info({:unregister_caller, caller}, state) when is_caller(caller) do
state = Map.delete(state, caller)

{:noreply, state}
end

#############
# Public API

@spec count(caller()) :: count()
def count(caller \\ self()) do
GenServer.call(__MODULE__, {:count, caller})
end

@spec increment() :: :ok
def increment() do
caller = self()
send(__MODULE__, {:increment, caller})

:ok
end

@spec register_caller(caller()) :: :ok
def register_caller(caller) when is_caller(caller) do
send(__MODULE__, {:register_caller, caller})
end

@spec unregister_caller(caller()) :: :ok
def unregister_caller(caller) when is_caller(caller) do
send(__MODULE__, {:unregister_caller, caller})
end

##################
# Private helpers

defp new_counter() do
:counters.new(1, [:atomics])
end

defp get_counter_ref(state, caller)
when is_map(state) and is_caller(caller) and is_map_key(state, caller) do
Map.fetch!(state, caller)
end

defp get_counter_ref(%{default: default_counter_ref} = state, caller)
when is_map(state) and is_caller(caller) do
callers = callers(caller)

Enum.find_value(callers, default_counter_ref, fn the_caller ->
Map.get(state, the_caller)
end)
end

defp callers(pid) when is_pid(pid) do
case Process.info(pid, :dictionary) do
{:dictionary, dictionary} ->
Keyword.get(dictionary, :"$callers", [])

nil ->
[]
end
end
end
18 changes: 16 additions & 2 deletions lib/mix/tasks/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ defmodule Mix.Tasks.Docs do
* `--proglang` - Chooses the main programming language: "elixir"
or "erlang"

* `--warnings-as-errors` - Exits with non-zero exit code if any warnings are found

The command line options have higher precedence than the options
specified in your `mix.exs` file below.

Expand Down Expand Up @@ -317,7 +319,8 @@ defmodule Mix.Tasks.Docs do
language: :string,
open: :boolean,
output: :string,
proglang: :string
proglang: :string,
warnings_as_errors: :boolean
]

@aliases [
Expand Down Expand Up @@ -385,7 +388,18 @@ defmodule Mix.Tasks.Docs do
browser_open(index)
end

index
warning_counter_count = ExDoc.WarningCounter.count()

if options[:warnings_as_errors] == true and warning_counter_count > 0 do
Mix.shell().info([
:red,
"Doc generation failed due to warnings while using the --warnings-as-errors option"
])

exit({:shutdown, warning_counter_count})
else
index
end
end
end

Expand Down
Loading