Repository: akoutmos/doctor
Branch: master
Commit: 0e3750ca205b
Files: 50
Total size: 143.3 KB
Directory structure:
gitextract_0by5ihkr/
├── .doctor.exs
├── .formatter.exs
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── master.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config/
│ └── config.exs
├── coveralls.json
├── lib/
│ ├── cli/
│ │ └── cli.ex
│ ├── config.ex
│ ├── docs.ex
│ ├── doctor.ex
│ ├── mix/
│ │ └── tasks/
│ │ ├── doctor.ex
│ │ ├── doctor.explain.ex
│ │ └── doctor.gen.config.ex
│ ├── module_information.ex
│ ├── module_report.ex
│ ├── report_utils.ex
│ ├── reporter.ex
│ ├── reporters/
│ │ ├── full.ex
│ │ ├── module_explain.ex
│ │ ├── output_utils.ex
│ │ ├── short.ex
│ │ └── summary.ex
│ └── specs.ex
├── mix.exs
└── test/
├── config_test.exs
├── configs/
│ ├── exceptions_moduledoc_not_required.exs
│ └── exceptions_moduledoc_required.exs
├── mix_doctor_test.exs
├── module_information_test.exs
├── module_report_test.exs
├── report_utils_test.exs
├── sample_files/
│ ├── all_docs.ex
│ ├── another_behaviour_module.ex
│ ├── behaviour_module.ex
│ ├── custom_behaviour_module.ex
│ ├── derive_protocol.ex
│ ├── exception.ex
│ ├── implement_protocol.ex
│ ├── nested_module.ex
│ ├── no_docs.ex
│ ├── no_struct_spec_module.ex
│ ├── opaque_struct_spec_module.ex
│ ├── partial_docs.ex
│ ├── struct_spec_module.ex
│ └── use_module.ex
└── test_helper.exs
================================================
FILE CONTENTS
================================================
================================================
FILE: .doctor.exs
================================================
%Doctor.Config{
ignore_modules: [],
ignore_paths: [
~r(^test/sample_files/.*)
],
min_module_doc_coverage: 80,
min_module_spec_coverage: 0,
min_overall_doc_coverage: 100,
min_overall_moduledoc_coverage: 100,
min_overall_spec_coverage: 0,
exception_moduledoc_required: true,
raise: false,
reporter: Doctor.Reporters.Full,
struct_type_spec_required: true,
umbrella: false
}
================================================
FILE: .formatter.exs
================================================
# Used by "mix format"
[
line_length: 120,
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
================================================
FILE: .github/FUNDING.yml
================================================
github: [akoutmos]
================================================
FILE: .github/workflows/master.yml
================================================
name: Doctor CI
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
static_analysis:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: "1.17"
otp-version: "27"
- name: Restore dependencies cache
uses: actions/cache@v2
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Mix Formatter
run: mix format --check-formatted
- name: Check for compiler warnings
run: mix compile --warnings-as-errors
- name: Doctor documentation checks
run: mix doctor
unit_test:
name: Run ExUnit tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: "1.17"
otp-version: "27"
- name: Restore dependencies cache
uses: actions/cache@v2
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: ExUnit tests
env:
MIX_ENV: test
run: mix coveralls.github
================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
doctor-*.tar
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.22.0] - 2024-10-30
### Changed
- Fix deprecated Elixir `Logger.warn()` to `Logger.warning()`.
- Changed minimum Elixir version to 1.14
## [0.21.0] - 2022-11-19
### Fixed
- Recognize protocol implementations
## [0.20.0] - 2022-10-11
### Fixed
- Recognize `@opaque` struct typespecs.
## [0.19.0] - 2022-7-19
### Fixed
- `mix doctor.explain` now works in umbrella projects
- Properly measure documentation coverage in nested modules
- Properly measure documentation with `__using__`
- Fix `@moduledoc` detection for older elixir versions
## [0.18.0] - 2021-5-27
- @doc false assumes no explicit spec and does not count against results
- Support for using macro (thanks to @pnezis)
- No reporting of missing docs for exception modules (thanks to @pnezis)
## [0.17.0] - 2021-1-11
- Bumped up the Elixir version due to use of Mix.Task.recursing/0
## [0.16.0] - 2020-12-27
- Fixed spec coverage bug
- Added ability to filter modules using Regex
## [0.15.0] - 2020-6-23
### Added
- Added `mix doctor.explain` command so that it is easier to debug why a particular module is failing validation
### Fixed
- Modules with behaviours that are aliased were not being counted properly
## [0.14.0] - 2020-3-19
### Added
- Additional configuration option struct_type_spec_required that checks for struct module type specs
## [0.13.0] - 2020-5-20
### Fixed
- Fixed spec coverage for behavior callbacks
## [0.12.0] - 2020-3-19
### Added
- Ability to aggregate umbrella results into one report
- Ability to pass custom path to config file
- CLI docs via `mix help doctor` and `mix help doctor.gen.config`
## [0.11.0] - 2020-1-29
### Added
- Ability to pass in a file name as a string for ignore_paths
## [0.10.0] - 2019-11-20
### Added
- Ability to raise from Mix when an error is encountered
## [0.9.0] - 2019-11-11
### Fixed
- .doctor.exs file not found at root of umbrella project
## [0.8.0] - 2019-6-20
### Fixed
- Fixed Decimal math when module contains no doc coverage
## [0.7.0] - 2019-6-10
### Added
- Travis CI and tests
### Fixed
- Incorrect reporting on failed modules
## [0.6.0] - 2019-6-5
### Added
- Short reporter
### Fixed
- Incorrect spec coverage
## [0.5.0] - 2019-6-2
### Changed
- Fixed counting issue when there are multiple modules in a single file
- Changed reporters around to be more DRY and share report calculation functionality
- Added tests for Doctor reporting functionality
## [0.4.0] - 2019-1-23
### Changed
- Loaded application vs starting the application to avoid Ecto errors connecting to DB during Doctor validation
## [0.3.0] - 2018-11-30
### Changed
- Updated dependencies and fixed depreciation warning
## [0.2.0] - 2018-11-30
### Fixed
- Umbrella project exit status code
## [0.1.0] - 2018-10-04
### Added
- Initial release of Doctor.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Alexander Koutmos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Doctor
[](https://hex.pm/packages/doctor)
[](https://github.com/akoutmos/doctor/actions)
[](https://coveralls.io/github/akoutmos/doctor?branch=master)
[](https://hexdocs.pm/doctor/)
[](https://hex.pm/packages/doctor)
[](https://github.com/akoutmos/doctor/blob/master/LICENSE)
Ensure that your documentation is healthy with Doctor! This library contains a mix task that you can run against your
project to generate a documentation coverage report. Items which are reported on include: the presence of module docs,
which functions do/don't have docs, which functions do/don't have typespecs, and if your struct modules provide
typespecs. You can generate a `.doctor.exs` config file to specify what thresholds are acceptable for your project. If
documentation coverage drops below your specified thresholds, the `mix doctor` task will return a non zero exit status.
The primary motivation with this tool is to have something simple which can be hooked up into CI to ensure that project
documentation standards are respected and upheld. This is particular useful in a team environment when you want to
maintain a minimum threshold for documentation coverage.
## Installation
Adding `:doctor` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:doctor, "~> 0.22.0", only: :dev}
]
end
```
Documentation can be found at [https://hexdocs.pm/doctor](https://hexdocs.pm/doctor).
## Comparison with other tools
There are a few tools in the Elixir ecosystem that overlap slightly in functionality with Doctor. It is useful for
you to know how Doctor differs from these tools and some use cases that Doctor serves.
**Credo**
[Credo](https://github.com/rrrene/credo) is a phenomenal library that can be used to perform a wide range of
static analysis checks against your codebase. It can check for lingering `IO.inspect()` statements, it can check for
unsafe atom conversions, and it can also check that the cyclomatic complexity of control statements is within a
particular range to name a few.
The one area where Doctor and Credo do overlap is that with either tool you have the capability to
enforce that `@moduledoc` attributes are present in modules. Given that this is the only overlap between the two tools,
I generally use both in my projects and perform both validations during CI/CD.
**Inch**
[Inch](https://github.com/rrrene/inch_ex) is another great tool written by René Föhring that is specifically
catered to analyzing a project's documentation (very much like Doctor). Inch will scan your project's source files and
check for the presence of function documentation and report back to you what grade it thinks your project has earned.
Inch does not appear to support checking for function typespecs, returning non-zero status codes when validation fails,
tuning thresholds via a configuration file, or checking for struct module typespecs. On the other hand, these were
things that were important to me personally and so I wrote Doctor to fill that void. In a team context, I find Doctor to
be invaluable in ensuring that a project maintains a certain level of documentation by failing CI/CD if certain
thresholds have not been met.
If I have misrepresented any of the aforementioned libraries...feel free to open up an issue :).
## Usage
Doctor comes with 2 mix tasks. One to run the documentation coverage report, and another to generate a `.doctor.exs` config file.
To run the doctor mix task and generate a report, run: `mix doctor`.
To generate a `.doctor.exs` config file with defaults, run: `mix doctor.gen.config`.
To get help documentation, run `mix help doctor` and `mix help doctor.gen.config`. The outputs of those help menus can be seen here:
Running `mix help doctor` yields:
```terminal
mix doctor
Doctor is a command line utility that can be used to ensure that your project
documentation remains healthy. For more in depth documentation on Doctor or to
file bug/feature requests, please check out https://github.com/akoutmos/doctor.
The mix doctor command supports the following CLI flags (all of these options
and more are also configurable from your .doctor.exs file). The following CLI
flags are supported:
--full When generating a Doctor report of your project, use
the Doctor.Reporters.Full reporter.
--short When generating a Doctor report of your project, use
the Doctor.Reporters.Short reporter.
--summary When generating a Doctor report of your project, use
the Doctor.Reporters.Summary reporter.
--raise If any of your modules fails Doctor validation, then
raise an error and return a non-zero exit status.
--failed If set only the failed modules will be reported. Works with
--full and --short options.
--umbrella By default, in an umbrella project, each app will be
evaluated independently against the specified thresholds
in your .doctor.exs file. This flag changes that behavior
by aggregating the results of all your umbrella apps,
and then comparing those results to the configured
thresholds.
```
Running `mix help doctor.gen.config` yields:
```terminal
mix doctor.gen.config
Doctor is a command line utility that can be used to ensure that your project
documentation remains healthy. For more in depth documentation on Doctor or to
file bug/feature requests, please check out https://github.com/akoutmos/doctor.
The mix doctor.gen.config command can be used to create a .doctor.exs file with
the default Doctor settings. The default file contents are:
%Doctor.Config{
ignore_modules: [],
ignore_paths: [],
min_module_doc_coverage: 40,
min_module_spec_coverage: 0,
min_overall_doc_coverage: 50,
min_overall_moduledoc_coverage: 100,
min_overall_spec_coverage: 0,
exception_moduledoc_required: true,
raise: false,
reporter: Doctor.Reporters.Full,
struct_type_spec_required: true,
umbrella: false
}
```
## Configuration
Below is a sample `.doctor.exs` file with some sample values for the various fields:
```elixir
%Doctor.Config{
ignore_modules: [],
ignore_paths: [],
min_module_doc_coverage: 40,
min_module_spec_coverage: 0,
min_overall_doc_coverage: 50,
min_overall_moduledoc_coverage: 100,
min_overall_spec_coverage: 0,
exception_moduledoc_required: true,
raise: false,
reporter: Doctor.Reporters.Full,
struct_type_spec_required: true,
umbrella: false
}
```
For the reporter field, the following reporters included with Doctor:
- `Doctor.Reporters.Full`
- `Doctor.Reporters.Short`
- `Doctor.Reporters.Summary`
## Sample reports
Report created for Doctor itself:
```text
$ mix doctor
Doctor file found. Loading configuration.
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Doc Cov Spec Cov Module File
Functions No Docs No Specs Module Doc Struct Spec
100% 0% Doctor.CLI lib/cli/cli.ex 2
0 2 Yes N/A
100% 0% Doctor.Config lib/config.ex 3
0 3 Yes Yes
100% 0% Doctor.Docs lib/docs.ex 1
0 1 Yes Yes
N/A N/A Doctor lib/doctor.ex 0
0 0 Yes N/A
100% 100% Mix.Tasks.Doctor lib/mix/tasks/doctor.ex 1
0 0 Yes N/A
100% 0% Mix.Tasks.Doctor.Gen.Config lib/mix/tasks/doctor.gen.config.ex 1
0 1 Yes N/A
100% 0% Doctor.ModuleInformation lib/module_information.ex 4
0 4 Yes Yes
100% 0% Doctor.ModuleReport lib/module_report.ex 1
0 1 Yes Yes
100% 0% Doctor.ReportUtils lib/report_utils.ex 9
0 9 Yes N/A
N/A N/A Doctor.Reporter lib/reporter.ex 0
0 0 Yes N/A
100% 0% Doctor.Reporters.Full lib/reporters/full.ex 1
0 1 Yes N/A
100% 0% Doctor.Reporters.OutputUtils lib/reporters/output_utils.ex 1
0 1 Yes N/A
100% 0% Doctor.Reporters.Short lib/reporters/short.ex 1
0 1 Yes N/A
100% 0% Doctor.Reporters.Summary lib/reporters/summary.ex 1
0 1 Yes N/A
100% 0% Doctor.Specs lib/specs.ex 1
0 1 Yes Yes
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Summary:
Passed Modules: 15
Failed Modules: 0
Total Doc Coverage: 100.0%
Total Spec Coverage: 3.7%
Doctor validation has passed!
```
Report created for Phoenix:
```text
$ mix doctor
Doctor file not found. Using defaults.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Doc Cov Spec Cov Module File Functions No Docs No Specs Module Doc
100% 0% Mix.Phoenix lib/mix/phoenix.ex 18 0 18 YES
0% 0% Mix.Phoenix.Context lib/mix/phoenix/context.ex 6 6 6 YES
63% 0% Mix.Phoenix.Schema lib/mix/phoenix/schema.ex 8 3 8 YES
100% 0% Mix.Tasks.Compile.Phoenix lib/mix/tasks/compile.phoenix.ex 2 0 2 YES
100% 0% Mix.Tasks.Phx.Digest.Clean lib/mix/tasks/phx.digest.clean.ex 1 0 1 YES
100% 0% Mix.Tasks.Phx.Digest lib/mix/tasks/phx.digest.ex 1 0 1 YES
100% 0% Mix.Tasks.Phx lib/mix/tasks/phx.ex 1 0 1 YES
100% 0% Mix.Tasks.Phx.Gen.Cert lib/mix/tasks/phx.gen.cert.ex 2 0 2 YES
100% 0% Mix.Tasks.Phx.Gen.Channel lib/mix/tasks/phx.gen.channel.ex 1 0 1 YES
86% 14% Mix.Tasks.Phx.Gen.Context lib/mix/tasks/phx.gen.context.ex 7 1 6 YES
100% 17% Mix.Tasks.Phx.Gen.Embedded lib/mix/tasks/phx.gen.embedded.ex 6 0 5 YES
100% 0% Mix.Tasks.Phx.Gen.Html lib/mix/tasks/phx.gen.html.ex 4 0 4 YES
100% 0% Mix.Tasks.Phx.Gen.Json lib/mix/tasks/phx.gen.json.ex 4 0 4 YES
100% 0% Mix.Tasks.Phx.Gen.Presence lib/mix/tasks/phx.gen.presence.ex 1 0 1 YES
100% 14% Mix.Tasks.Phx.Gen.Schema lib/mix/tasks/phx.gen.schema.ex 7 0 6 YES
100% 0% Mix.Tasks.Phx.Gen.Secret lib/mix/tasks/phx.gen.secret.ex 1 0 1 YES
100% 0% Mix.Tasks.Phx.Routes lib/mix/tasks/phx.routes.ex 1 0 1 YES
100% 0% Mix.Tasks.Phx.Server lib/mix/tasks/phx.server.ex 1 0 1 YES
100% 0% Phoenix lib/phoenix.ex 3 0 3 YES
100% 17% Phoenix.Channel lib/phoenix/channel.ex 12 0 10 YES
100% 18% Phoenix.Channel.Server lib/phoenix/channel/server.ex 17 0 14 YES
100% 0% Phoenix.CodeReloader lib/phoenix/code_reloader.ex 2 0 2 YES
40% 0% Phoenix.CodeReloader.Proxy lib/phoenix/code_reloader/proxy.ex 5 3 5 YES
33% 0% Phoenix.CodeReloader.Server lib/phoenix/code_reloader/server.ex 6 4 6 YES
88% 25% Phoenix.Config lib/phoenix/config.ex 8 1 6 YES
100% 52% Phoenix.Controller lib/phoenix/controller.ex 42 0 20 YES
100% 0% Phoenix.Controller.Pipeline lib/phoenix/controller/pipeline.ex 6 0 6 YES
100% 100% Phoenix.Digester lib/phoenix/digester.ex 2 0 0 YES
100% 0% Phoenix.Endpoint lib/phoenix/endpoint.ex 25 0 25 YES
100% 0% Phoenix.Endpoint.Cowboy2Adapter lib/phoenix/endpoint/cowboy2_adapter.ex 3 0 3 YES
0% 0% Phoenix.Endpoint.Cowboy2Handler lib/phoenix/endpoint/cowboy2_handler.ex 5 5 5 YES
100% 0% Phoenix.Endpoint.CowboyAdapter lib/phoenix/endpoint/cowboy_adapter.ex 2 0 2 YES
0% 0% Phoenix.Endpoint.CowboyWebSocket lib/phoenix/endpoint/cowboy_websocket.ex 8 8 8 YES
100% 0% Phoenix.Endpoint.RenderErrors lib/phoenix/endpoint/render_errors.ex 3 0 3 YES
93% 0% Phoenix.Endpoint.Supervisor lib/phoenix/endpoint/supervisor.ex 15 1 15 YES
0% 0% Phoenix.Endpoint.Watcher lib/phoenix/endpoint/watcher.ex 2 2 2 YES
NA NA Plug.Exception.Phoenix.ActionClauseErro lib/phoenix/exceptions.ex 0 0 0 NO
NA NA Phoenix.NotAcceptableError lib/phoenix/exceptions.ex 0 0 0 YES
100% 0% Phoenix.MissingParamError lib/phoenix/exceptions.ex 1 0 1 YES
0% 0% Phoenix.ActionClauseError lib/phoenix/exceptions.ex 2 2 2 NO
60% 0% Phoenix.Logger lib/phoenix/logger.ex 5 2 5 YES
83% 100% Phoenix.Naming lib/phoenix/naming.ex 6 1 0 YES
NA NA Phoenix.Param.Map lib/phoenix/param.ex 0 0 0 NO
NA NA Phoenix.Param.Integer lib/phoenix/param.ex 0 0 0 NO
NA NA Phoenix.Param.BitString lib/phoenix/param.ex 0 0 0 NO
NA NA Phoenix.Param.Atom lib/phoenix/param.ex 0 0 0 NO
NA NA Phoenix.Param.Any lib/phoenix/param.ex 0 0 0 NO
0% 0% Phoenix.Param lib/phoenix/param.ex 1 1 1 YES
100% 0% Phoenix.Presence lib/phoenix/presence.ex 17 0 17 YES
NA NA Phoenix.Router.NoRouteError lib/phoenix/router.ex 0 0 0 YES
100% 0% Phoenix.Router lib/phoenix/router.ex 11 0 11 YES
100% 0% Phoenix.Router.ConsoleFormatter lib/phoenix/router/console_formatter.ex 1 0 1 YES
95% 0% Phoenix.Router.Helpers lib/phoenix/router/helpers.ex 20 1 20 YES
100% 0% Phoenix.Router.Resource lib/phoenix/router/resource.ex 1 0 1 YES
100% 20% Phoenix.Router.Route lib/phoenix/router/route.ex 5 0 4 YES
100% 0% Phoenix.Router.Scope lib/phoenix/router/scope.ex 9 0 9 YES
NA NA Phoenix.Socket.InvalidMessageError lib/phoenix/socket.ex 0 0 0 YES
57% 0% Phoenix.Socket lib/phoenix/socket.ex 14 6 14 YES
NA NA Phoenix.Socket.Reply lib/phoenix/socket/message.ex 0 0 0 YES
100% 0% Phoenix.Socket.Message lib/phoenix/socket/message.ex 1 0 1 YES
NA NA Phoenix.Socket.Broadcast lib/phoenix/socket/message.ex 0 0 0 YES
50% 0% Phoenix.Socket.PoolSupervisor lib/phoenix/socket/pool_supervisor.ex 4 2 4 YES
NA NA Phoenix.Socket.Serializer lib/phoenix/socket/serializer.ex 0 0 0 YES
0% 0% Phoenix.Socket.V1.JSONSerializer lib/phoenix/socket/serializers/v1_json_serializer.ex 3 3 3 YES
0% 0% Phoenix.Socket.V2.JSONSerializer lib/phoenix/socket/serializers/v2_json_serializer.ex 3 3 3 YES
100% 0% Phoenix.Socket.Transport lib/phoenix/socket/transport.ex 6 0 6 YES
NA NA Phoenix.Template.UndefinedError lib/phoenix/template.ex 0 0 0 YES
100% 45% Phoenix.Template lib/phoenix/template.ex 11 0 6 YES
0% 0% Phoenix.Template.EExEngine lib/phoenix/template/eex_engine.ex 1 1 1 YES
NA NA Phoenix.Template.Engine lib/phoenix/template/engine.ex 0 0 0 YES
0% 0% Phoenix.Template.ExsEngine lib/phoenix/template/exs_engine.ex 1 1 1 YES
NA NA Phoenix.ChannelTest.NoopSerializer lib/phoenix/test/channel_test.ex 0 0 0 YES
100% 11% Phoenix.ChannelTest lib/phoenix/test/channel_test.ex 19 0 17 YES
100% 94% Phoenix.ConnTest lib/phoenix/test/conn_test.ex 17 0 1 YES
100% 0% Phoenix.Token lib/phoenix/token.ex 2 0 2 YES
67% 0% Phoenix.Transports.LongPoll lib/phoenix/transports/long_poll.ex 3 1 3 YES
50% 0% Phoenix.Transports.LongPoll.Server lib/phoenix/transports/long_poll_server.ex 4 2 4 YES
0% 0% Phoenix.Transports.WebSocket lib/phoenix/transports/websocket.ex 2 2 2 YES
100% 0% Phoenix.View lib/phoenix/view.ex 9 0 9 YES
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Summary:
Passed Modules: 72
Failed Modules: 7
Total Doc Coverage: 85.1%
Total Spec Coverage: 15.3%
Doctor validation has passed!
```
================================================
FILE: config/config.exs
================================================
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
import Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.
# You can configure your application as:
#
# config :doctor, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:doctor, :key)
#
# You can also configure a 3rd-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env()}.exs"
================================================
FILE: coveralls.json
================================================
{
"minimum_coverage": 75,
"skip_files": [
"test/sample_files/"
]
}
================================================
FILE: lib/cli/cli.ex
================================================
defmodule Doctor.CLI do
@moduledoc """
Provides the various CLI task entry points and CLI arg parsing.
"""
alias Mix.Project
alias Doctor.{ModuleInformation, ModuleReport, ReportUtils}
alias Doctor.Reporters.ModuleExplain
@doc """
Given the CLI arguments, run the report on the project,
"""
def generate_module_report_list(args) do
# Using the project's app name, fetch all the modules associated with the app
Project.config()
|> Keyword.get(:app)
|> get_application_modules()
|> Enum.filter(fn module -> String.starts_with?(to_string(module), "Elixir.") end)
# Fetch the module information from the list of application modules
|> Enum.map(&generate_module_entry/1)
# Filter out any files/modules that were specified in the config
|> Enum.reject(fn module_info -> filter_ignore_modules(module_info.module, args.ignore_modules) end)
|> Enum.reject(fn module_info -> filter_ignore_paths(module_info.file_relative_path, args.ignore_paths) end)
# Asynchronously get the user defined functions from the modules
|> Enum.map(&async_fetch_user_defined_functions/1)
|> Enum.map(&Task.await(&1, 15_000))
# Build report struct for each module
|> Enum.sort(&(&1.file_relative_path < &2.file_relative_path))
|> Enum.map(&ModuleReport.build/1)
end
@doc """
Generate a report for a single project module.
"""
def generate_single_module_report(module_name, args) do
Project.config()
|> Keyword.get(:app)
|> get_application_modules()
|> Enum.find(:not_found, &(inspect(&1) == module_name))
|> case do
:not_found ->
:not_found
module ->
module
|> generate_module_entry()
|> async_fetch_user_defined_functions()
|> Task.await(15_000)
|> ModuleExplain.generate_report(args)
end
end
@doc """
Given a list of individual module reports, process each item in the
list with the configured reporter and return a pass or fail boolean
"""
def process_module_report_list(module_report_list, args) do
# Invoke the configured module reporter and return whether Doctor validation passed/failed
args.reporter.generate_report(module_report_list, args)
ReportUtils.doctor_report_passed?(module_report_list, args)
end
defp generate_module_entry(module) do
module
|> Code.fetch_docs()
|> ModuleInformation.build(module)
end
defp async_fetch_user_defined_functions(%ModuleInformation{} = module_info) do
Task.async(fn ->
module_info
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
end)
end
defp get_application_modules(application) do
# Compile and load the application
Mix.Task.run("compile")
Application.load(application)
# Get all the modules in the application
{:ok, modules} = :application.get_key(application, :modules)
modules
end
defp filter_ignore_paths(file_relative_path, ignore_paths) do
ignore_paths
|> Enum.reduce_while(false, fn pattern, _acc ->
compare_ignore_path(file_relative_path, pattern)
end)
end
defp compare_ignore_path(file_relative_path, %Regex{} = ignore_pattern) do
if Regex.match?(ignore_pattern, file_relative_path) do
{:halt, true}
else
{:cont, false}
end
end
defp compare_ignore_path(file_relative_path, ignore_string) when is_bitstring(ignore_string) do
if file_relative_path == ignore_string do
{:halt, true}
else
{:cont, false}
end
end
defp compare_ignore_path(_, ignore_value),
do: raise("Encountered invalid ignore_paths entry: #{inspect(ignore_value)}")
defp filter_ignore_modules(module, ignore_modules) do
ignore_modules
|> Enum.reduce_while(false, fn pattern, _acc ->
compare_ignore_module(module, pattern)
end)
end
defp compare_ignore_module(module, %Regex{} = ignore_pattern) do
module_as_string =
module
|> Atom.to_string()
|> String.trim_leading("Elixir.")
if Regex.match?(ignore_pattern, module_as_string) do
{:halt, true}
else
{:cont, false}
end
end
defp compare_ignore_module(module, ignore_module) when is_atom(ignore_module) do
if module == ignore_module do
{:halt, true}
else
{:cont, false}
end
end
defp compare_ignore_module(_, ignore_value),
do: raise("Encountered invalid ignore_module entry: #{inspect(ignore_value)}")
end
================================================
FILE: lib/config.ex
================================================
defmodule Doctor.Config do
@moduledoc """
This module defines a struct which houses all the
configuration data for Doctor.
"""
@config_file ".doctor.exs"
require Logger
alias __MODULE__
@typedoc """
* `:min_module_doc_coverage` - Minimum ratio of @doc vs public functions
per module.
* `:min_overall_doc_coverage` - Minimum ratio of @doc vs public functions
across the codebase.
* `:min_overall_moduledoc_coverage` - Minimum ratio of @moduledoc to modules
across the codebase.
* `:moduledoc_required` - If true, `:min_overall_moduledoc_coverage` is
automatically set to 100%. Deprecated.
"""
@type t :: %Config{
ignore_modules: [Regex.t() | String.t()],
ignore_paths: [Regex.t() | module()],
min_module_doc_coverage: integer() | float(),
min_module_spec_coverage: integer() | float(),
min_overall_doc_coverage: integer() | float(),
min_overall_moduledoc_coverage: integer() | float(),
min_overall_spec_coverage: integer() | float(),
moduledoc_required: boolean(),
exception_moduledoc_required: boolean() | nil,
raise: boolean(),
reporter: module(),
struct_type_spec_required: boolean(),
umbrella: boolean(),
failed: false
}
defstruct ignore_modules: [],
ignore_paths: [],
min_module_doc_coverage: 40,
min_module_spec_coverage: 0,
min_overall_doc_coverage: 50,
min_overall_moduledoc_coverage: 100,
min_overall_spec_coverage: 0,
moduledoc_required: nil,
exception_moduledoc_required: true,
raise: false,
reporter: Doctor.Reporters.Full,
struct_type_spec_required: true,
umbrella: false,
failed: false
@doc """
Create a new Config struct from a map, keyword list or preexisting Config.
"""
@spec new(keyword | map) :: Config.t()
def new(attrs \\ %{}) do
config =
case attrs do
%Config{} = c -> c
map_or_keyword -> struct(Config, map_or_keyword)
end
interpret_moduledoc_required(config)
end
@doc """
Returns true if a specific module should fail validation if it lacks a
moduledoc."
"""
@spec moduledoc_required?(t) :: boolean
def moduledoc_required?(%{min_overall_moduledoc_coverage: 100}), do: true
def moduledoc_required?(_), do: false
@doc """
Get the configuration defaults as a string
"""
def config_defaults_as_string do
config = quote do: unquote(%Config{})
config
|> Macro.to_string()
|> Code.format_string!()
|> to_string()
|> String.replace(~r/\s+moduledoc_required:.*/, "")
end
@doc """
Get the configuration file name
"""
def config_file, do: @config_file
# If `:moduledoc_required` is defined in the config, warn the user about the
# deprecation. In a future version, the struct key and associated
# backwards-compatibility code could be removed.
@spec interpret_moduledoc_required(Config.t()) :: Config.t()
defp interpret_moduledoc_required(%{moduledoc_required: nil} = config) do
config
end
defp interpret_moduledoc_required(%{moduledoc_required: true} = config) do
warn_deprecation(true, 100)
%{config | min_overall_moduledoc_coverage: 100}
end
defp interpret_moduledoc_required(%{moduledoc_required: false} = config) do
warn_deprecation(false, 0)
%{config | min_overall_moduledoc_coverage: 0}
end
defp warn_deprecation(_bool, val) do
Logger.warning("""
:moduledoc_required in #{Config.config_file()} is a deprecated option. \
Now running with the equivalent :min_overall_moduledoc_coverage #{val} \
but you should replace the deprecated option with the new one to avoid \
this warning.\
""")
end
end
================================================
FILE: lib/docs.ex
================================================
defmodule Doctor.Docs do
@moduledoc """
This module defines a struct which houses all the
documentation data for module functions.
"""
alias __MODULE__
@type t :: %Docs{
kind: atom(),
name: atom(),
arity: integer(),
doc: map()
}
defstruct ~w(kind name arity doc)a
@doc """
Build the Docs struct from the results of Code.fetch_docs/0
"""
def build({{kind, name, arity}, _annotation, _signature, doc, _metadata}) do
%Docs{
kind: kind,
name: name,
arity: arity,
doc: doc
}
end
end
================================================
FILE: lib/doctor.ex
================================================
defmodule Doctor do
@moduledoc """
Doctor is a utility which aims to provide insights into the health of your project's documentation.
In addition to be a useful development time tool, Doctor can also be useful during CI/CD and can block
merges and releases if the documentation coverage is not up to spec.
Doctor comes with sane defaults out of the box, but if you wish to customize its settings, feel free to
create your own .doctor.exs file.
"""
end
================================================
FILE: lib/mix/tasks/doctor.ex
================================================
defmodule Mix.Tasks.Doctor do
@moduledoc """
Doctor is a command line utility that can be used to ensure that your project
documentation remains healthy. For more in depth documentation on Doctor or to
file bug/feature requests, please check out https://github.com/akoutmos/doctor.
The `mix doctor` command supports the following CLI flags (all of these options
and more are also configurable from your `.doctor.exs` file). The following CLI
flags are supported:
```
--config_file Provide a relative or absolute path to a `.doctor.exs`
file to use during the execution of the mix command.
--full When generating a Doctor report of your project, use
the Doctor.Reporters.Full reporter.
--short When generating a Doctor report of your project, use
the Doctor.Reporters.Short reporter.
--summary When generating a Doctor report of your project, use
the Doctor.Reporters.Summary reporter.
--raise If any of your modules fails Doctor validation, then
raise an error and return a non-zero exit status.
--failed If set, only the failed modules will be reported. Works with
--full and --short options.
--umbrella By default, in an umbrella project, each app will be
evaluated independently against the specified thresholds
in your .doctor.exs file. This flag changes that behavior
by aggregating the results of all your umbrella apps,
and then comparing those results to the configured
thresholds.
```
"""
use Mix.Task
alias Doctor.{CLI, Config}
alias Doctor.Reporters.{Full, Short, Summary}
@shortdoc "Documentation coverage report"
@recursive true
@umbrella_accumulator Doctor.Umbrella
@impl true
def run(args) do
cli_arg_opts = parse_cli_args(args)
config_file_opts = load_config_file(cli_arg_opts)
# Aggregate all of the various options sources
# Precedence order is:
# default < config file < cli args
config =
config_file_opts
|> Map.merge(cli_arg_opts)
|> Config.new()
if config.umbrella do
run_umbrella(config)
else
run_default(config)
end
end
defp run_umbrella(config) do
module_report_list = CLI.generate_module_report_list(config)
acc_pid =
case Process.whereis(@umbrella_accumulator) do
nil -> init_umbrella_acc(config)
pid -> pid
end
Agent.update(acc_pid, fn acc ->
acc ++ module_report_list
end)
:ok
end
defp run_default(config) do
result =
config
|> CLI.generate_module_report_list()
|> CLI.process_module_report_list(config)
unless result do
System.at_exit(fn _ ->
exit({:shutdown, 1})
end)
if config.raise do
Mix.raise("Doctor validation has failed and raised an error")
end
end
:ok
end
defp init_umbrella_acc(config) do
{:ok, pid} = Agent.start_link(fn -> [] end, name: @umbrella_accumulator)
System.at_exit(fn _ ->
module_report_list = Agent.get(pid, & &1)
Agent.stop(pid)
result = CLI.process_module_report_list(module_report_list, config)
unless result do
if config.raise do
Mix.raise("Doctor validation has failed and raised an error")
end
exit({:shutdown, 1})
end
end)
pid
end
defp load_config_file(%{config_file_path: file_path} = _cli_args) do
full_path = Path.expand(file_path)
if File.exists?(full_path) do
Mix.shell().info("Doctor file found. Loading configuration.")
{config, _bindings} = Code.eval_file(full_path)
config
else
Mix.shell().error("Doctor file not found at path \"#{full_path}\". Using defaults.")
%{}
end
end
defp load_config_file(_) do
# If we are performing this operation on an umbrella app then look to
# the project root for the config file
file =
if Mix.Task.recursing?() do
Path.join(["..", "..", Config.config_file()])
else
Config.config_file()
end
if File.exists?(file) do
Mix.shell().info("Doctor file found. Loading configuration.")
{config, _bindings} = Code.eval_file(file)
config
else
Mix.shell().info("Doctor file not found. Using defaults.")
%{}
end
end
defp parse_cli_args(args) do
{parsed_args, _args, _invalid} =
OptionParser.parse(args,
strict: [
full: :boolean,
short: :boolean,
summary: :boolean,
raise: :boolean,
failed: :boolean,
umbrella: :boolean,
config_file: :string
]
)
parsed_args
|> Enum.reduce(%{}, fn
{:full, true}, acc -> Map.merge(acc, %{reporter: Full})
{:short, true}, acc -> Map.merge(acc, %{reporter: Short})
{:summary, true}, acc -> Map.merge(acc, %{reporter: Summary})
{:raise, true}, acc -> Map.merge(acc, %{raise: true})
{:failed, true}, acc -> Map.merge(acc, %{failed: true})
{:umbrella, true}, acc -> Map.merge(acc, %{umbrella: true})
{:config_file, file_path}, acc -> Map.merge(acc, %{config_file_path: file_path})
_unexpected_arg, acc -> acc
end)
end
end
================================================
FILE: lib/mix/tasks/doctor.explain.ex
================================================
defmodule Mix.Tasks.Doctor.Explain do
@moduledoc """
Figuring out why a particular module failed Doctor validation can sometimes
be a bit difficult when the relevant information is embedded within a table with
other validation results.
The `mix doctor.explain` command has only a single required argument. That argument
is the name of the module that you wish to get a detailed report of. For example you
could run the following from the terminal:
```
$ mix doctor.explain MyApp.Some.Module
```
To generate a report like this:
```
Doctor file found. Loading configuration.
Function @doc @spec
-------------------------------
generate_report/2 ✗ ✗
Module Results:
Doc Coverage: 0.0% --> Your config has a 'min_module_doc_coverage' value of 80
Spec Coverage: 0.0%
Has Module Doc: ✓
Has Struct Spec: N/A
```
In addition, the following CLI flags are supported (similarly to the `mix doctor`
command):
```
--config-file Provide a relative or absolute path to a `.doctor.exs`
file to use during the execution of the mix command.
--raise If any of your modules fails Doctor validation, then
raise an error and return a non-zero exit status.
```
To use these command line args you would do something like so:
```
$ mix doctor.explain --raise --config_file /some/path/to/some/.doctor.exs MyApp.Some.Module
```
Note that `mix doctor.explain` takes a module name instead of a file path since you can
define multiple modules in a single file.
"""
use Mix.Task
alias Doctor.{CLI, Config}
@shortdoc "Debug why a particular module is failing validation"
@recursive true
@umbrella_accumulator Doctor.Umbrella
@impl true
def run(args) do
{cli_arg_opts, args} = parse_cli_args(args)
config_file_opts = load_config_file(cli_arg_opts)
# Aggregate all of the various options sources
# Precedence order is:
# default < config file < cli args
config =
config_file_opts
|> Map.merge(cli_arg_opts)
|> Config.new()
# Get the module name from args
module_name =
case args do
[module] ->
module
_error ->
raise "Invalid Argument: mix doctor.explain takes only a single module name as an argument"
end
if config.umbrella do
run_umbrella(module_name, config)
else
run_default(module_name, config)
end
end
defp run_umbrella(module_name, config) do
acc_pid =
case Process.whereis(@umbrella_accumulator) do
nil -> init_umbrella_acc(module_name, config)
pid -> pid
end
case CLI.generate_single_module_report(module_name, config) do
:not_found ->
:ok
result ->
Agent.update(acc_pid, fn %{result: acc_result} ->
%{found: true, result: acc_result && result}
end)
end
:ok
end
defp run_default(module_name, config) do
case CLI.generate_single_module_report(module_name, config) do
:not_found ->
raise "Could not find module #{inspect(module_name)} in application"
result ->
unless result do
System.at_exit(fn _ ->
exit({:shutdown, 1})
end)
if config.raise do
Mix.raise("Doctor validation has failed and raised an error")
end
end
end
:ok
end
defp init_umbrella_acc(module_name, config) do
{:ok, pid} = Agent.start_link(fn -> %{found: false, result: true} end, name: @umbrella_accumulator)
System.at_exit(fn _ ->
acc = Agent.get(pid, & &1)
Agent.stop(pid)
report_umbrella_result(acc, module_name, config)
end)
pid
end
defp report_umbrella_result(%{found: false}, module_name, _config) do
raise "Could not find module #{inspect(module_name)} in application"
end
defp report_umbrella_result(%{result: true}, _module_name, _config), do: :ok
defp report_umbrella_result(_acc, _module_name, config) do
if config.raise do
Mix.raise("Doctor validation has failed and raised an error")
end
exit({:shutdown, 1})
end
defp load_config_file(%{config_file_path: file_path} = _cli_args) do
full_path = Path.expand(file_path)
if File.exists?(full_path) do
Mix.shell().info("Doctor file found. Loading configuration.")
{config, _bindings} = Code.eval_file(full_path)
config
else
Mix.shell().error("Doctor file not found at path \"#{full_path}\". Using defaults.")
%{}
end
end
defp load_config_file(_) do
# If we are performing this operation on an umbrella app then look to
# the project root for the config file
file =
if Mix.Task.recursing?() do
Path.join(["..", "..", Config.config_file()])
else
Config.config_file()
end
if File.exists?(file) do
Mix.shell().info("Doctor file found. Loading configuration.")
{config, _bindings} = Code.eval_file(file)
config
else
Mix.shell().info("Doctor file not found. Using defaults.")
%{}
end
end
defp parse_cli_args(args) do
{parsed_args, args, _invalid} =
OptionParser.parse(args,
strict: [
raise: :boolean,
config_file: :string
]
)
parsed_args =
parsed_args
|> Enum.reduce(%{}, fn
{:raise, true}, acc -> Map.merge(acc, %{raise: true})
{:config_file, file_path}, acc -> Map.merge(acc, %{config_file_path: file_path})
_unexpected_arg, acc -> acc
end)
{parsed_args, args}
end
end
================================================
FILE: lib/mix/tasks/doctor.gen.config.ex
================================================
defmodule Mix.Tasks.Doctor.Gen.Config do
@moduledoc """
Doctor is a command line utility that can be used to ensure that your project
documentation remains healthy. For more in depth documentation on Doctor or to
file bug/feature requests, please check out https://github.com/akoutmos/doctor.
The `mix doctor.gen.config` command can be used to create a `.doctor.exs` file
with the default Doctor settings. The default file contents are:
```
%Doctor.Config{
ignore_modules: [],
ignore_paths: [],
min_module_doc_coverage: 40,
min_module_spec_coverage: 0,
min_overall_doc_coverage: 50,
min_overall_moduledoc_coverage: 100,
min_overall_spec_coverage: 0,
exception_moduledoc_required: true,
raise: false,
reporter: Doctor.Reporters.Full,
struct_type_spec_required: true,
umbrella: false
}
```
"""
use Mix.Task
alias Mix.Shell.IO
alias Doctor.Config
@shortdoc "Creates a .doctor.exs config file with defaults"
@doc """
This Mix task generates a .doctor.exs configuration file
"""
@impl true
def run(_args) do
create_file =
if File.exists?(Config.config_file()) do
IO.yes?("An existing Doctor config file already exists. Overwrite?")
else
true
end
if create_file do
create_config_file()
IO.info("Successfully created .doctor.exs file.")
else
IO.info("Did not create .doctor.exs file.")
end
end
defp create_config_file do
File.cwd!()
|> Path.join(Config.config_file())
|> File.write(Config.config_defaults_as_string())
end
end
================================================
FILE: lib/module_information.ex
================================================
defmodule Doctor.ModuleInformation do
@moduledoc """
This module defines a struct which houses all the
documentation data for an entire module.
"""
alias __MODULE__
alias Doctor.{Docs, Specs}
@type t :: %ModuleInformation{
module: module(),
behaviours: [module()],
file_full_path: String.t(),
file_relative_path: String.t(),
file_ast: list(),
docs_version: atom(),
module_doc: map(),
metadata: map(),
docs: [%Docs{}],
specs: list(),
user_defined_functions: [{atom(), integer(), atom() | boolean()}],
struct_type_spec: atom() | boolean(),
properties: Keyword.t()
}
defstruct ~w(
module
file_full_path
file_relative_path
file_ast
docs_version
module_doc
metadata
docs
specs
user_defined_functions
behaviours
struct_type_spec
properties
)a
@doc """
Breaks down the docs format entry returned from Code.fetch_docs(MODULE)
"""
def build({docs_version, _annotation, _language, _format, module_doc, metadata, docs}, module) do
{:ok, module_specs} = Code.Typespec.fetch_specs(module)
%ModuleInformation{
module: module,
behaviours: get_module_behaviours(module),
file_full_path: get_full_file_path(module),
file_relative_path: get_relative_file_path(module),
file_ast: nil,
docs_version: docs_version,
module_doc: module_doc,
metadata: metadata,
docs: Enum.map(docs, &Docs.build/1),
specs: Enum.map(module_specs, &Specs.build/1),
user_defined_functions: nil,
struct_type_spec: contains_struct_type_spec?(module),
properties: [
is_exception: is_exception?(module),
is_protocol_implementation: is_protocol_implementation?(module)
]
}
end
@doc """
Given the provided module, read the file from which the module was generated and
convert the file to an AST.
"""
def load_file_ast(%ModuleInformation{} = module_info) do
ast =
module_info.file_full_path
|> File.read!()
|> Code.string_to_quoted!()
%{module_info | file_ast: ast}
end
@doc """
Checks the provided module for a __struct__ function which is injected into the module
whenever you use `defstruct`
"""
def contains_struct_type_spec?(module) do
cond do
is_exception?(module) ->
:not_struct
is_struct?(module) ->
{:ok, specs} = Code.Typespec.fetch_types(module)
Enum.any?(specs, fn
{:type, {:t, _, _}} -> true
{:opaque, {:t, _, _}} -> true
_ -> false
end)
true ->
:not_struct
end
end
defp is_struct?(module) do
function_exported?(module, :__struct__, 0) or function_exported?(module, :__struct__, 1)
end
defp is_exception?(module) when is_atom(module) do
function_exported?(module, :__struct__, 0) and :__exception__ in Map.keys(module.__struct__())
end
defp is_protocol_implementation?(module) when is_atom(module) do
function_exported?(module, :__impl__, 1)
end
@doc """
Given a ModuleInformation struct with the AST loaded, fetch all of the author defined functions
"""
def load_user_defined_functions(%ModuleInformation{} = module_info) do
{_ast, %{modules: modules}} =
Macro.traverse(
module_info.file_ast,
%{modules: %{}, stack: []},
&parse_ast_node_for_defmodules/2,
&pop_module_stack/2
)
{_ast, %{functions: functions}} =
modules
|> Map.get(module_info.module)
|> Macro.traverse(
%{functions: [], last_impl: :none, nesting_level: 0},
&parse_ast_node_for_def/2,
&unnest/2
)
%{module_info | user_defined_functions: Enum.uniq(functions)}
|> load_using_docs_and_specs(modules)
end
defp load_using_docs_and_specs(%ModuleInformation{} = module_info, modules) do
{_ast, using} =
modules
|> Map.get(module_info.module)
|> Macro.prewalk(%{using: :none}, &parse_ast_for_using/2)
acc = %{
last_doc: :none,
last_spec: :none,
using_docs: [],
using_specs: []
}
{_ast, extra} =
using[:using]
|> Macro.prewalk(acc, &parse_ast_using_node/2)
%{
module_info
| specs: module_info.specs ++ extra.using_specs,
docs: module_info.docs ++ extra.using_docs
}
end
defp get_module_behaviours(module) do
{_module, bin, _beam_file_path} = :code.get_object_code(module)
case :beam_lib.chunks(bin, [:attributes]) do
{:ok, {^module, attributes}} ->
attributes
|> Keyword.get(:attributes, [])
|> Keyword.get(:behaviour, [])
_ ->
[]
end
end
defp get_full_file_path(module) do
module.module_info()
|> Keyword.get(:compile)
|> Keyword.get(:source)
|> to_string()
end
defp get_relative_file_path(module) do
module
|> get_full_file_path()
|> Path.relative_to(File.cwd!())
end
defp parse_ast_node_for_def({definition, _defmodule_line, _body} = ast, %{nesting_level: level} = acc)
when definition in [:defimpl, :defmodule, :defprotocol] do
{ast, Map.put(acc, :nesting_level, level + 1)}
end
defp parse_ast_node_for_def(ast, %{nesting_level: level} = acc) when level > 1 do
{ast, acc}
end
defp parse_ast_node_for_def({:@, _line_number, [{:doc, _, [false]}]} = ast, acc) do
updated_acc = Map.put(acc, :last_impl, false)
{ast, updated_acc}
end
defp parse_ast_node_for_def({:@, _line_number, [{:impl, _, impl_def}]} = ast, acc) do
normalized_impl = normalize_impl(impl_def)
updated_acc = Map.put(acc, :last_impl, normalized_impl)
{ast, updated_acc}
end
defp parse_ast_node_for_def(
{:def, _def_line, [{:when, _line_when, [{function_name, _function_line, args}, _guard]}, _do_block]} = ast,
%{last_impl: impl} = acc
) do
function_arity = get_function_arity(args)
updated_acc = update_acc_for_def(acc, function_name, function_arity, impl)
{ast, updated_acc}
end
defp parse_ast_node_for_def(
{:def, _def_line, [{function_name, _function_line, args}, _do_block]} = ast,
%{last_impl: impl} = acc
) do
function_arity = get_function_arity(args)
updated_acc = update_acc_for_def(acc, function_name, function_arity, impl)
{ast, updated_acc}
end
defp parse_ast_node_for_def(
{:def, _def_line, [{function_name, _function_line, args}]} = ast,
%{last_impl: impl} = acc
) do
function_arity = get_function_arity(args)
updated_acc = update_acc_for_def(acc, function_name, function_arity, impl)
{ast, updated_acc}
end
defp parse_ast_node_for_def(ast, acc) do
{ast, acc}
end
defp unnest({definition, _defmodule_line, _body} = ast, %{nesting_level: level} = acc)
when definition in [:defmodule, :defprotocol] do
{ast, Map.put(acc, :nesting_level, level - 1)}
end
defp unnest(ast, acc) do
{ast, acc}
end
defp update_acc_for_def(acc, function_name, function_arity, last_impl) do
impl =
case last_impl do
:none ->
acc[:functions]
|> Enum.filter(fn {name, arity, _impl} -> name == function_name and arity == function_arity end)
|> Enum.at(0, {function_name, function_arity, :none})
|> Kernel.elem(2)
last_impl ->
last_impl
end
acc
|> Map.put(:last_impl, :none)
|> Map.update(:functions, [], fn functions ->
[{function_name, function_arity, impl} | functions]
end)
end
defp normalize_impl([value]) when is_boolean(value) do
value
end
defp normalize_impl([{:__aliases__, _, module}]) do
Module.concat(module)
end
defp normalize_impl(value) do
value
end
defp parse_ast_node_for_defmodules(
{definition, _defmodule_line, [{:__aliases__, _line_num, module}, _do_block]} = ast,
%{modules: modules, stack: stack} = acc
)
when definition in [:defmodule, :defprotocol] do
parent = List.first(stack)
full_module_name = Module.concat(List.wrap(parent) ++ module)
updated_acc =
acc
|> Map.put(:modules, Map.put(modules, full_module_name, ast))
|> Map.put(:stack, [full_module_name | stack])
{ast, updated_acc}
end
defp parse_ast_node_for_defmodules(ast, acc) do
{ast, acc}
end
defp pop_module_stack(
{definition, _defmodule_line, _body} = ast,
%{stack: [_current | rest]} = acc
)
when definition in [:defmodule, :defprotocol] do
{ast, Map.put(acc, :stack, rest)}
end
defp pop_module_stack(ast, acc) do
{ast, acc}
end
defp get_function_arity(nil), do: 0
defp get_function_arity(args), do: length(args)
defp parse_ast_for_using({:defmacro, _macro_line, [{:__using__, _line, _args}, do_block]} = ast, _acc),
do: {ast, %{using: do_block}}
defp parse_ast_for_using(ast, acc), do: {ast, acc}
defp parse_ast_using_node(
{:@, _doc_line, [{:doc, _line, [doc]}]} = ast,
acc
),
do: {ast, Map.put(acc, :last_doc, doc)}
defp parse_ast_using_node(
{:@, _spec_line, [{:spec, _line, _spec_info}]} = ast,
acc
),
do: {ast, Map.put(acc, :last_spec, true)}
defp parse_ast_using_node(
{:def, _def_line, [{:when, _line_when, [{function_name, _function_line, args}, _guard]}, _do_block]} = ast,
acc
) do
{ast, update_acc_for_using(function_name, args, acc)}
end
defp parse_ast_using_node(
{:def, _def_line, [{function_name, _function_line, args}, _do_block]} = ast,
acc
) do
{ast, update_acc_for_using(function_name, args, acc)}
end
defp parse_ast_using_node(
{:def, _def_line, [{function_name, _function_line, args}]} = ast,
acc
) do
{ast, update_acc_for_using(function_name, args, acc)}
end
defp parse_ast_using_node(ast, acc), do: {ast, acc}
defp update_acc_for_using(function_name, args, acc) do
function_arity = get_function_arity(args)
function_spec =
if acc.last_spec != :none do
[%Doctor.Specs{arity: function_arity, name: function_name}]
else
[]
end
function_doc =
if acc.last_doc != :none do
[
%Doctor.Docs{
arity: function_arity,
doc: %{"en" => acc.last_doc},
kind: :function,
name: function_name
}
]
else
[]
end
%{
last_doc: :none,
last_spec: :none,
using_docs: acc.using_docs ++ function_doc,
using_specs: acc.using_specs ++ function_spec
}
end
end
================================================
FILE: lib/module_report.ex
================================================
defmodule Doctor.ModuleReport do
@moduledoc """
This module exposes a struct which encapsulates all the results for a doctor report. Whether
the module has a moduledoc, what the doc coverage is, the number of author defined functions,
and so on.
"""
alias __MODULE__
alias Doctor.ModuleInformation
@type t :: %ModuleReport{
doc_coverage: Decimal.t(),
spec_coverage: Decimal.t(),
file: String.t(),
module: String.t(),
functions: integer(),
missed_docs: integer(),
missed_specs: integer(),
has_module_doc: boolean(),
has_struct_type_spec: atom() | boolean(),
is_protocol_implementation: boolean(),
properties: Keyword.t()
}
defstruct ~w(
doc_coverage
spec_coverage
file
module
functions
missed_docs
missed_specs
has_module_doc
has_struct_type_spec
is_protocol_implementation
properties
)a
@doc """
Given a ModuleInformation struct with the necessary fields completed,
build the report.
"""
def build(%ModuleInformation{} = module_info) do
%ModuleReport{
doc_coverage: calculate_doc_coverage(module_info),
spec_coverage: calculate_spec_coverage(module_info),
file: module_info.file_relative_path,
module: generate_module_name(module_info.module),
functions: length(module_info.user_defined_functions),
missed_docs: calculate_missed_docs(module_info),
missed_specs: calculate_missed_specs(module_info),
has_module_doc: has_module_doc?(module_info),
has_struct_type_spec: module_info.struct_type_spec,
is_protocol_implementation: is_protocol_implementation?(module_info),
properties: module_info.properties
}
end
defp generate_module_name(module) do
module
|> Module.split()
|> Enum.join(".")
end
defp calculate_missed_docs(module_info) do
function_arity_list =
Enum.map(module_info.user_defined_functions, fn {function, arity, _impl} ->
{function, arity}
end)
docs_arity_list = Enum.map(module_info.docs, fn doc -> {doc.name, doc.arity} end)
functions_not_in_docs =
Enum.count(function_arity_list, fn fun ->
fun not in docs_arity_list
end)
functions_without_docs =
Enum.count(module_info.docs, fn doc ->
{doc.name, doc.arity} in function_arity_list and doc.doc == :none
end)
functions_not_in_docs + functions_without_docs
end
defp calculate_doc_coverage(module_info) do
total = length(module_info.user_defined_functions)
missed = calculate_missed_docs(module_info)
if total > 0 do
(total - missed)
|> Decimal.div(total)
|> Decimal.mult(100)
else
nil
end
end
defp calculate_missed_specs(module_info) do
function_specs =
module_info.specs
|> Enum.map(fn spec ->
{spec.name, spec.arity}
end)
Enum.count(module_info.user_defined_functions, fn {function, arity, impl} ->
cond do
{function, arity} in function_specs ->
false
is_boolean(impl) and impl and module_info.behaviours != [] ->
false
is_atom(impl) and impl != :none and module_info.behaviours != [] ->
false
true ->
true
end
end)
end
defp calculate_spec_coverage(module_info) do
total = length(module_info.user_defined_functions)
missed = calculate_missed_specs(module_info)
if total > 0 do
(total - missed)
|> Decimal.div(total)
|> Decimal.mult(100)
else
nil
end
end
defp has_module_doc?(module_info) do
module_info.module_doc not in [:none, %{}]
end
defp is_protocol_implementation?(module_info) do
Keyword.get(module_info.properties, :is_protocol_implementation)
end
end
================================================
FILE: lib/report_utils.ex
================================================
defmodule Doctor.ReportUtils do
@moduledoc """
This module provides some utility functions for use in report generators.
"""
alias Doctor.{Config, ModuleReport}
@doc """
Given a list of module reports, count the total number of functions
"""
def count_total_functions(module_report_list) do
module_report_list
|> Enum.reduce(0, fn module_report, acc ->
module_report.functions + acc
end)
end
@doc """
Given a list of module reports, count the total number of documented functions
"""
def count_total_documented_functions(module_report_list) do
module_report_list
|> Enum.reduce(0, fn module_report, acc ->
module_documented_functions = module_report.functions - module_report.missed_docs
module_documented_functions + acc
end)
end
@doc """
Given a list of module reports, count the total number of speced functions
"""
def count_total_speced_functions(module_report_list) do
module_report_list
|> Enum.reduce(0, fn module_report, acc ->
module_speced_functions = module_report.functions - module_report.missed_specs
module_speced_functions + acc
end)
end
@doc """
Given a list of module reports, count the total number of passed modules
"""
def count_total_passed_modules(module_report_list, %Config{} = config) do
module_report_list
|> Enum.count(fn module_report ->
module_passed_validation?(module_report, config)
end)
end
@doc """
Given a list of module reports, count the total number of failed modules
"""
def count_total_failed_modules(module_report_list, %Config{} = config) do
module_report_list
|> Enum.count(fn module_report ->
not module_passed_validation?(module_report, config)
end)
end
@doc """
Calculate the overall doc coverage in the codebase
"""
def calc_overall_doc_coverage(module_report_list) do
total_functions = count_total_functions(module_report_list)
documented_functions = count_total_documented_functions(module_report_list)
if total_functions > 0 do
documented_functions
|> Decimal.div(total_functions)
|> Decimal.mult(100)
else
Decimal.new(0)
end
end
@doc """
Calculate the ratio of modules which have a moduledoc.
"""
def calc_overall_moduledoc_coverage(module_report_list) do
{all_modules, with_moduledoc} =
Enum.reduce(module_report_list, {0, 0}, fn
%{is_protocol_implementation: true}, {acc_all, acc_with} -> {acc_all, acc_with}
%{has_module_doc: true}, {acc_all, acc_with} -> {acc_all + 1, acc_with + 1}
%{has_module_doc: false}, {acc_all, acc_with} -> {acc_all + 1, acc_with}
end)
with_moduledoc
|> Decimal.div(all_modules)
|> Decimal.mult(100)
end
@doc """
Calculate the overall spec coverage in the codebase
"""
def calc_overall_spec_coverage(module_report_list) do
total_functions = count_total_functions(module_report_list)
speced_functions = count_total_speced_functions(module_report_list)
if total_functions > 0 do
speced_functions
|> Decimal.div(total_functions)
|> Decimal.mult(100)
else
Decimal.new(0)
end
end
@doc """
Checks whether the provided module passed validation
"""
def module_passed_validation?(
%ModuleReport{
doc_coverage: doc_coverage,
spec_coverage: spec_coverage,
has_struct_type_spec: has_struct_type_spec
} = module_report,
%Config{} = config
) do
doc_cov = calc_coverage_pass(doc_coverage, config.min_module_doc_coverage)
spec_cov = calc_coverage_pass(spec_coverage, config.min_module_spec_coverage)
passed_module_doc = valid_module_doc?(module_report, config)
passed_struct_type_spec =
if config.struct_type_spec_required and has_struct_type_spec != :not_struct,
do: has_struct_type_spec,
else: true
doc_cov and spec_cov and passed_struct_type_spec and passed_module_doc
end
defp valid_module_doc?(%ModuleReport{is_protocol_implementation: true}, _config) do
true
end
defp valid_module_doc?(%ModuleReport{properties: properties} = module_report, config) do
if Keyword.get(properties, :is_exception) do
if config.exception_moduledoc_required do
module_report.has_module_doc
else
true
end
else
if Config.moduledoc_required?(config),
do: module_report.has_module_doc,
else: true
end
end
@doc """
Check whether Doctor overall has passed or failed validation
"""
def doctor_report_passed?(module_report_list, config) do
[] == doctor_report_errors(module_report_list, config)
end
@doc """
Check whether Doctor overall has passed or failed validation
"""
@spec doctor_report_errors([Doctor.ModuleReport.t()], Config.t()) :: [String.t()]
def doctor_report_errors(module_report_list, %Config{} = config) do
msg = fn
true, _msg -> []
false, msg -> [msg]
end
all_modules =
module_report_list
|> Enum.reduce_while([], fn module_report, _acc ->
if module_passed_validation?(module_report, config) do
{:cont, []}
else
{:halt, ["one or more highlighted modules above is unhealthy"]}
end
end)
overall_doc_cov =
module_report_list
|> calc_overall_doc_coverage()
|> Decimal.to_float()
|> Kernel.>=(config.min_overall_doc_coverage)
|> msg.("overall @doc coverage is below #{config.min_overall_doc_coverage}")
overall_moduledoc_cov =
module_report_list
|> calc_overall_moduledoc_coverage()
|> Decimal.to_float()
|> Kernel.>=(config.min_overall_moduledoc_coverage)
|> msg.("overall @moduledoc coverage is below #{config.min_overall_moduledoc_coverage}")
overall_spec_cov =
module_report_list
|> calc_overall_spec_coverage()
|> Decimal.to_float()
|> Kernel.>=(config.min_overall_spec_coverage)
|> msg.("overall @spec coverage is below #{config.min_overall_spec_coverage}")
all_modules ++ overall_doc_cov ++ overall_moduledoc_cov ++ overall_spec_cov
end
defp calc_coverage_pass(coverage, threshold) when not is_nil(coverage) do
Decimal.to_float(coverage) >= threshold
end
defp calc_coverage_pass(_coverage, _threshold), do: true
end
================================================
FILE: lib/reporter.ex
================================================
defmodule Doctor.Reporter do
@moduledoc """
Defines the behaviour for a reporter
"""
@type module_reports :: [Doctor.ModuleReport.t()]
@callback generate_report(module_reports, any()) :: :ok | :error
end
================================================
FILE: lib/reporters/full.ex
================================================
defmodule Doctor.Reporters.Full do
@moduledoc """
This reporter generates a full documentation coverage report and lists
all the files in the project along with whether they pass or fail.
"""
@behaviour Doctor.Reporter
alias Doctor.{Reporters.OutputUtils, ReportUtils}
alias Elixir.IO.ANSI
@doc_cov_width 9
@spec_cov_width 10
@module_width 41
@file_width 58
@functions_width 11
@missed_docs_width 9
@missed_specs_width 10
@module_doc_width 12
@struct_type_spec_width 11
@doc """
Generate a full Doctor report and print to STDOUT
"""
@impl true
def generate_report(module_reports, args) do
print_divider()
print_header()
Enum.each(module_reports, fn module_report ->
doc_cov = massage_coverage(module_report.doc_coverage)
spec_cov = massage_coverage(module_report.spec_coverage)
module_doc = massage_module_doc(module_report)
struct_type_spec = massage_struct_type_spec(module_report.has_struct_type_spec)
output_line =
OutputUtils.generate_table_line([
{doc_cov, @doc_cov_width},
{spec_cov, @spec_cov_width},
{module_report.module, @module_width},
{module_report.file, @file_width},
{module_report.functions, @functions_width},
{module_report.missed_docs, @missed_docs_width},
{module_report.missed_specs, @missed_specs_width},
{module_doc, @module_doc_width},
{struct_type_spec, @struct_type_spec_width, 0}
])
if ReportUtils.module_passed_validation?(module_report, args) do
unless args.failed do
Mix.shell().info(output_line)
end
else
Mix.shell().info(ANSI.red() <> output_line <> ANSI.reset())
end
end)
overall_errors = ReportUtils.doctor_report_errors(module_reports, args)
overall_passed = ReportUtils.count_total_passed_modules(module_reports, args)
overall_failed = ReportUtils.count_total_failed_modules(module_reports, args)
overall_doc_coverage = ReportUtils.calc_overall_doc_coverage(module_reports)
overall_moduledoc_coverage = ReportUtils.calc_overall_moduledoc_coverage(module_reports)
overall_spec_coverage = ReportUtils.calc_overall_spec_coverage(module_reports)
print_footer(
overall_errors,
overall_passed,
overall_failed,
overall_doc_coverage,
overall_moduledoc_coverage,
overall_spec_coverage
)
end
defp print_header() do
output_header =
OutputUtils.generate_table_line([
{"Doc Cov", @doc_cov_width},
{"Spec Cov", @spec_cov_width},
{"Module", @module_width},
{"File", @file_width},
{"Functions", @functions_width},
{"No Docs", @missed_docs_width},
{"No Specs", @missed_specs_width},
{"Module Doc", @module_doc_width},
{"Struct Spec", @struct_type_spec_width, 0}
])
Mix.shell().info(output_header)
end
defp print_divider do
"-"
|> String.duplicate(171)
|> Mix.shell().info()
end
defp print_footer(errors, passed, failed, doc_coverage, moduledoc_coverage, spec_coverage) do
doc_coverage = Decimal.round(doc_coverage, 1)
moduledoc_coverage = Decimal.round(moduledoc_coverage, 1)
spec_coverage = Decimal.round(spec_coverage, 1)
print_divider()
Mix.shell().info("Summary:\n")
Mix.shell().info("Passed Modules: #{passed}")
Mix.shell().info("Failed Modules: #{failed}")
Mix.shell().info("Total Doc Coverage: #{doc_coverage}%")
Mix.shell().info("Total Moduledoc Coverage: #{moduledoc_coverage}%")
Mix.shell().info("Total Spec Coverage: #{spec_coverage}%\n")
msg =
case errors do
[] ->
"Doctor validation has passed!"
[err] ->
"""
#{ANSI.red()}Doctor validation has failed because #{err}.#{ANSI.reset()}
"""
errs ->
"""
#{ANSI.red()}Doctor validation has failed because:
* #{Enum.map_join(errs, ".\n * ", &String.capitalize(&1))}.\
#{ANSI.reset()}
"""
end
Mix.shell().info(msg)
end
defp massage_coverage(coverage) do
if coverage do
"#{Decimal.round(coverage)}%"
else
"N/A"
end
end
defp massage_module_doc(%{is_protocol_implementation: true}), do: "N/A"
defp massage_module_doc(%{has_module_doc: true}), do: "Yes"
defp massage_module_doc(%{has_module_doc: false}), do: "No"
defp massage_struct_type_spec(:not_struct), do: "N/A"
defp massage_struct_type_spec(true), do: "Yes"
defp massage_struct_type_spec(false), do: "No"
end
================================================
FILE: lib/reporters/module_explain.ex
================================================
defmodule Doctor.Reporters.ModuleExplain do
@moduledoc """
This module produces a report for a single project module. This
is useful when you need to figure out exactly why a particular
module failed validation. The only validations that are taken
into account during this report are single module validations.
In other words, the only thing that is checked are things that
pertain to a single module like:
- `min_module_doc_coverage`
- `min_module_spec_coverage`
- `moduledoc_required`
- `exception_moduledoc_required`
- `struct_type_spec_required`
"""
alias Doctor.{Config, Docs, Specs}
alias Doctor.{ModuleInformation, ModuleReport}
alias Doctor.Reporters.OutputUtils
@doc """
Generate the output for a single module report
"""
def generate_report(%ModuleInformation{} = module_information, %Config{} = config) do
module_report = ModuleReport.build(module_information)
user_defined_functions = module_information.user_defined_functions
module_docs = module_information.docs
module_specs = module_information.specs
# Get max function name length
# 13 is picked as the starting acc as that is the length of "Function Name"
# which is the column header
max_length =
Enum.reduce(user_defined_functions, 13, fn {function, _arity, _impl}, acc ->
length =
function
|> Atom.to_string()
|> String.length()
if length > acc, do: length, else: acc
end)
|> Kernel.+(5)
# Print table header
generate_header(max_length)
OutputUtils.print_divider(max_length + 11)
# Print per function information
Enum.each(user_defined_functions, fn {function, arity, impl} ->
function_name =
function
|> Atom.to_string()
|> Kernel.<>("/#{arity}")
|> OutputUtils.gen_fixed_width_string(max_length)
has_doc =
function
|> has_doc(arity, impl, module_docs)
|> OutputUtils.print_pass_or_fail()
|> OutputUtils.gen_fixed_width_string(6)
has_spec =
function
|> has_spec(arity, impl, module_specs)
|> OutputUtils.print_pass_or_fail()
|> OutputUtils.gen_fixed_width_string(6)
Mix.shell().info("#{function_name}#{has_doc}#{has_spec}")
end)
# Print module summary info
Mix.shell().info("\nModule Results:")
print_doc_coverage(module_report, config)
print_spec_coverage(module_report, config)
print_module_doc(module_report, config)
print_struct_spec(module_report, config)
# Determine whether the module passed or failed
valid_module?(module_report, config)
end
defp valid_module?(%ModuleReport{is_protocol_implementation: true}, _config), do: true
defp valid_module?(module_report, config) do
valid_struct_spec?(module_report, config) and
valid_moduledoc?(module_report, config) and
valid_doc_coverage?(module_report, config) and
valid_spec_coverage?(module_report, config)
end
defp valid_struct_spec?(module_report, config) do
(config.struct_type_spec_required and module_report.has_struct_type_spec == :not_struct) or
module_report.has_struct_type_spec
end
defp valid_moduledoc?(%ModuleReport{is_protocol_implementation: true}, _config), do: true
defp valid_moduledoc?(module_report, config) do
(not config.exception_moduledoc_required and module_report.properties[:is_exception]) or
(Config.moduledoc_required?(config) and module_report.has_module_doc)
end
defp valid_doc_coverage?(%ModuleReport{is_protocol_implementation: true}, _config), do: true
defp valid_doc_coverage?(module_report, config) do
doc_coverage(module_report) >= config.min_module_doc_coverage
end
defp valid_spec_coverage?(module_report, config) do
spec_coverage(module_report) >= config.min_module_spec_coverage
end
defp doc_coverage(module_report) do
module_report.doc_coverage
|> Decimal.round(1)
|> Decimal.to_float()
end
defp spec_coverage(module_report) do
module_report.spec_coverage
|> Decimal.round(1)
|> Decimal.to_float()
end
defp print_struct_spec(%ModuleReport{} = module_report, %Config{} = config) do
if valid_struct_spec?(module_report, config) do
OutputUtils.print_success(
" Has Struct Spec: #{OutputUtils.print_pass_or_fail(module_report.has_struct_type_spec)}"
)
else
OutputUtils.print_error(
" Has Struct Spec: #{OutputUtils.print_pass_or_fail(module_report.has_struct_type_spec)} --> Your config has a 'struct_type_spec_required' value of true"
)
end
end
defp print_module_doc(%ModuleReport{is_protocol_implementation: true}, %Config{} = _config) do
OutputUtils.print_success(" Has Module Doc: N/A")
end
defp print_module_doc(%ModuleReport{} = module_report, %Config{} = config) do
if valid_moduledoc?(module_report, config) do
OutputUtils.print_success(" Has Module Doc: #{OutputUtils.print_pass_or_fail(module_report.has_module_doc)}")
else
config_option =
case module_report.properties[:is_exception] do
true ->
"an 'exception_moduledoc_required'"
_ ->
"a 'moduledoc_required'"
end
OutputUtils.print_error(
" Has Module Doc: #{OutputUtils.print_pass_or_fail(module_report.has_module_doc)} --> Your config has #{config_option} value of true"
)
end
end
defp print_doc_coverage(%ModuleReport{is_protocol_implementation: true}, %Config{} = _config) do
OutputUtils.print_success(" Doc Coverage: N/A")
end
defp print_doc_coverage(%ModuleReport{} = module_report, %Config{} = config) do
doc_coverage = doc_coverage(module_report)
if doc_coverage >= config.min_module_doc_coverage do
OutputUtils.print_success(" Doc Coverage: #{doc_coverage}%")
else
OutputUtils.print_error(
" Doc Coverage: #{doc_coverage}% --> Your config has a 'min_module_doc_coverage' value of #{config.min_module_doc_coverage}"
)
end
end
defp print_spec_coverage(%ModuleReport{is_protocol_implementation: true}, %Config{} = _config) do
OutputUtils.print_success(" Spec Coverage: N/A")
end
defp print_spec_coverage(%ModuleReport{} = module_report, %Config{} = config) do
spec_coverage = spec_coverage(module_report)
if spec_coverage >= config.min_module_spec_coverage do
OutputUtils.print_success(" Spec Coverage: #{spec_coverage}%")
else
OutputUtils.print_error(
" Spec Coverage: #{spec_coverage}% --> Your config has a 'min_module_spec_coverage' value of #{config.min_module_spec_coverage}"
)
end
end
defp generate_header(function_name_length) do
output_line =
OutputUtils.generate_table_line([
{"Function", function_name_length},
{"@doc", 6},
{"@spec", 7}
])
Mix.shell().info("\n#{output_line}")
end
defp has_doc(function, arity, :none, module_docs) do
Enum.any?(module_docs, fn
%Docs{arity: ^arity, name: ^function, doc: doc} when doc != :none ->
true
_ ->
false
end)
end
defp has_doc(_, _, _, _) do
true
end
defp has_spec(function, arity, :none, module_specs) do
Enum.any?(module_specs, fn
%Specs{arity: ^arity, name: ^function} -> true
_ -> false
end)
end
defp has_spec(_, _, _, _) do
true
end
end
================================================
FILE: lib/reporters/output_utils.ex
================================================
defmodule Doctor.Reporters.OutputUtils do
@moduledoc """
This module provides convenience functions for use when generating
reports
"""
alias Elixir.IO.ANSI
@doc """
Generate a line in a table with the given width and padding. Expects a
list with either a 2 or 3 element tuple.
"""
def generate_table_line(line_data) do
line_data
|> Enum.reduce("", fn
{value, width}, acc ->
"#{acc}#{gen_fixed_width_string(value, width)}"
{value, width, padding}, acc ->
"#{acc}#{gen_fixed_width_string(value, width, padding)}"
end)
end
@doc """
Prints a divider of a given length
"""
def print_divider(length) do
"-"
|> String.duplicate(length)
|> Mix.shell().info()
end
@doc """
Prints a checkmark of an X if true of false is provided respectively
"""
def print_pass_or_fail(true), do: "\u2713"
def print_pass_or_fail(false), do: "\u2717"
def print_pass_or_fail(:not_struct), do: "N/A"
@doc """
Prints a string in red
"""
def print_error(string), do: Mix.shell().info(ANSI.red() <> string <> ANSI.reset())
@doc """
Prints a string in green
"""
def print_success(string), do: Mix.shell().info(ANSI.green() <> string <> ANSI.reset())
@doc """
Generate a string with a configure amount of width and padding
"""
def gen_fixed_width_string(value, width, padding \\ 2)
def gen_fixed_width_string(value, width, padding) when is_atom(value) do
value
|> Atom.to_string()
|> gen_fixed_width_string(width, padding)
end
def gen_fixed_width_string(value, width, padding) when is_integer(value) do
value
|> Integer.to_string()
|> gen_fixed_width_string(width, padding)
end
def gen_fixed_width_string(value, width, padding) do
sub_string_length = width - (padding + 1)
value
|> String.slice(0..sub_string_length)
|> String.pad_trailing(width)
end
end
================================================
FILE: lib/reporters/short.ex
================================================
defmodule Doctor.Reporters.Short do
@moduledoc """
This reporter generates a full documentation coverage report and lists
all the files in the project along with whether they pass or fail.
"""
@behaviour Doctor.Reporter
alias Elixir.IO.ANSI
alias Doctor.{Reporters.OutputUtils, ReportUtils}
@doc_cov_width 9
@spec_cov_width 10
@module_width 41
@functions_width 11
@module_doc_width 12
@struct_type_spec_width 11
@doc """
Generate a short Doctor report and print to STDOUT
"""
@impl true
def generate_report(module_reports, args) do
print_divider()
print_header()
Enum.each(module_reports, fn module_report ->
doc_cov = massage_coverage(module_report.doc_coverage)
spec_cov = massage_coverage(module_report.spec_coverage)
module_doc = massage_module_doc(module_report)
struct_type_spec = massage_struct_type_spec(module_report.has_struct_type_spec)
output_line =
OutputUtils.generate_table_line([
{doc_cov, @doc_cov_width},
{spec_cov, @spec_cov_width},
{module_report.functions, @functions_width},
{module_report.module, @module_width},
{module_doc, @module_doc_width},
{struct_type_spec, @struct_type_spec_width, 0}
])
if ReportUtils.module_passed_validation?(module_report, args) do
unless args.failed do
Mix.shell().info(output_line)
end
else
Mix.shell().info(ANSI.red() <> output_line <> ANSI.reset())
end
end)
overall_pass = ReportUtils.doctor_report_passed?(module_reports, args)
overall_passed = ReportUtils.count_total_passed_modules(module_reports, args)
overall_failed = ReportUtils.count_total_failed_modules(module_reports, args)
overall_doc_coverage = ReportUtils.calc_overall_doc_coverage(module_reports)
overall_moduledoc_coverage = ReportUtils.calc_overall_moduledoc_coverage(module_reports)
overall_spec_coverage = ReportUtils.calc_overall_spec_coverage(module_reports)
print_footer(
overall_pass,
overall_passed,
overall_failed,
overall_doc_coverage,
overall_moduledoc_coverage,
overall_spec_coverage
)
end
defp print_header() do
output_header =
OutputUtils.generate_table_line([
{"Doc Cov", @doc_cov_width},
{"Spec Cov", @spec_cov_width},
{"Functions", @functions_width},
{"Module", @module_width},
{"Module Doc", @module_doc_width},
{"Struct Spec", @struct_type_spec_width, 0}
])
Mix.shell().info(output_header)
end
defp print_divider do
"-"
|> String.duplicate(94)
|> Mix.shell().info()
end
defp print_footer(pass, passed, failed, doc_coverage, moduledoc_coverage, spec_coverage) do
doc_coverage = Decimal.round(doc_coverage, 1)
moduledoc_coverage = Decimal.round(moduledoc_coverage, 1)
spec_coverage = Decimal.round(spec_coverage, 1)
print_divider()
Mix.shell().info("Summary:\n")
Mix.shell().info("Passed Modules: #{passed}")
Mix.shell().info("Failed Modules: #{failed}")
Mix.shell().info("Total Doc Coverage: #{doc_coverage}%")
Mix.shell().info("Total Moduledoc Coverage: #{moduledoc_coverage}%")
Mix.shell().info("Total Spec Coverage: #{spec_coverage}%\n")
if pass do
Mix.shell().info("Doctor validation has passed!")
else
Mix.shell().info(ANSI.red() <> "Doctor validation has failed!" <> ANSI.reset())
end
end
defp massage_coverage(coverage) do
if coverage do
"#{Decimal.round(coverage)}%"
else
"N/A"
end
end
defp massage_module_doc(%{is_protocol_implementation: true}), do: "N/A"
defp massage_module_doc(%{has_module_doc: true}), do: "Yes"
defp massage_module_doc(%{has_module_doc: false}), do: "No"
defp massage_struct_type_spec(:not_struct), do: "N/A"
defp massage_struct_type_spec(true), do: "Yes"
defp massage_struct_type_spec(false), do: "No"
end
================================================
FILE: lib/reporters/summary.ex
================================================
defmodule Doctor.Reporters.Summary do
@moduledoc """
This reporter generates a short summary documentation coverage report
and lists overall how many modules passed/failed.
"""
@behaviour Doctor.Reporter
alias Elixir.IO.ANSI
alias Doctor.ReportUtils
@doc """
Generate a short summary Doctor report and print to STDOUT
"""
@impl true
def generate_report(module_reports, args) do
overall_pass = ReportUtils.doctor_report_passed?(module_reports, args)
overall_passed = ReportUtils.count_total_passed_modules(module_reports, args)
overall_failed = ReportUtils.count_total_failed_modules(module_reports, args)
overall_doc_coverage = ReportUtils.calc_overall_doc_coverage(module_reports)
overall_moduledoc_coverage = ReportUtils.calc_overall_moduledoc_coverage(module_reports)
overall_spec_coverage = ReportUtils.calc_overall_spec_coverage(module_reports)
print_footer(
overall_pass,
overall_passed,
overall_failed,
overall_doc_coverage,
overall_moduledoc_coverage,
overall_spec_coverage
)
end
defp print_divider do
"-"
|> String.duplicate(45)
|> Mix.shell().info()
end
defp print_footer(pass, passed, failed, doc_coverage, moduledoc_coverage, spec_coverage) do
doc_coverage = Decimal.round(doc_coverage, 1)
moduledoc_coverage = Decimal.round(moduledoc_coverage, 1)
spec_coverage = Decimal.round(spec_coverage, 1)
print_divider()
Mix.shell().info("Summary:\n")
Mix.shell().info("Passed Modules: #{passed}")
Mix.shell().info("Failed Modules: #{failed}")
Mix.shell().info("Total Doc Coverage: #{doc_coverage}%")
Mix.shell().info("Total Moduledoc Coverage: #{moduledoc_coverage}%")
Mix.shell().info("Total Spec Coverage: #{spec_coverage}%\n")
if pass do
Mix.shell().info("Doctor validation has passed!")
else
Mix.shell().info(ANSI.red() <> "Doctor validation has failed!" <> ANSI.reset())
end
end
end
================================================
FILE: lib/specs.ex
================================================
defmodule Doctor.Specs do
@moduledoc """
This module defines a struct which houses all the
documentation data for function specs.
"""
alias __MODULE__
@type t :: %Specs{
name: atom(),
arity: integer()
}
defstruct ~w(name arity)a
@doc """
Build a spec definition for each result from Code.Typespec.fetch_specs/1
"""
def build({{name, arity}, _spec}) do
%Specs{
name: name,
arity: arity
}
end
end
================================================
FILE: mix.exs
================================================
defmodule Doctor.MixProject do
use Mix.Project
@source_url "https://github.com/akoutmos/doctor"
def project do
[
app: :doctor,
version: "0.22.0",
elixir: "~> 1.14",
name: "Doctor",
source_url: @source_url,
homepage_url: "https://hex.pm/packages/doctor",
description: "Simple utility to create documentation coverage reports",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
docs: [
main: "readme",
extras: ["README.md", "CHANGELOG.md"]
],
package: package(),
deps: deps(),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
coveralls: :test,
"coveralls.detail": :test,
"coveralls.post": :test,
"coveralls.html": :test,
"coveralls.github": :test
]
]
end
def application do
[
extra_applications: [:logger]
]
end
defp elixirc_paths(:test), do: ["lib", "test/sample_files"]
defp elixirc_paths(_), do: ["lib"]
defp package() do
[
name: "doctor",
files: ~w(lib mix.exs README.md LICENSE CHANGELOG.md),
licenses: ["MIT"],
links: %{
"GitHub" => @source_url,
"Changelog" => "https://hexdocs.pm/doctor/changelog.html",
"Sponsor" => "https://github.com/sponsors/akoutmos"
}
]
end
defp deps do
[
# Production dependencies
{:decimal, "~> 2.0"},
# Development and testing dependencies
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
{:excoveralls, "~> 0.14", only: :test, runtime: false}
]
end
end
================================================
FILE: test/config_test.exs
================================================
defmodule Doctor.ConfigTest do
use ExUnit.Case, async: true
alias Doctor.Config
test "config_defaults_as_string" do
assert %Doctor.Config{
exception_moduledoc_required: true,
failed: false,
ignore_modules: [],
ignore_paths: [],
min_module_doc_coverage: 40,
min_module_spec_coverage: 0,
min_overall_doc_coverage: 50,
min_overall_moduledoc_coverage: 100,
min_overall_spec_coverage: 0,
raise: false,
reporter: Doctor.Reporters.Full,
struct_type_spec_required: true,
umbrella: false
} == Config.config_defaults_as_string() |> Code.eval_string() |> elem(0)
end
end
================================================
FILE: test/configs/exceptions_moduledoc_not_required.exs
================================================
%Doctor.Config{
ignore_modules: [],
ignore_paths: [],
min_module_doc_coverage: 80,
min_module_spec_coverage: 0,
min_overall_doc_coverage: 100,
min_overall_moduledoc_coverage: 100,
min_overall_spec_coverage: 0,
exception_moduledoc_required: false,
raise: false,
reporter: Doctor.Reporters.Full,
struct_type_spec_required: true,
umbrella: false
}
================================================
FILE: test/configs/exceptions_moduledoc_required.exs
================================================
%Doctor.Config{
ignore_modules: [],
ignore_paths: [],
min_module_doc_coverage: 80,
min_module_spec_coverage: 0,
min_overall_doc_coverage: 100,
min_overall_moduledoc_coverage: 100,
min_overall_spec_coverage: 0,
exception_moduledoc_required: true,
raise: false,
reporter: Doctor.Reporters.Full,
struct_type_spec_required: true,
umbrella: false
}
================================================
FILE: test/mix_doctor_test.exs
================================================
defmodule Mix.Tasks.DoctorTest do
use ExUnit.Case, async: false
setup_all do
original_shell = Mix.shell()
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(original_shell)
end)
end
describe "mix doctor" do
test "should output the full report when no params are provided" do
Mix.Tasks.Doctor.run([])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
[
"---------------------------------------------------------------------------------------------------------------------------------------------------------------------------"
],
[
"Doc Cov Spec Cov Module File Functions No Docs No Specs Module Doc Struct Spec"
],
[
"100% 0% Doctor.CLI lib/cli/cli.ex 3 0 3 Yes N/A "
],
[
"100% 50% Doctor.Config lib/config.ex 4 0 2 Yes Yes "
],
[
"100% 0% Doctor.Docs lib/docs.ex 1 0 1 Yes Yes "
],
[
"N/A N/A Doctor lib/doctor.ex 0 0 0 Yes N/A "
],
[
"100% 100% Mix.Tasks.Doctor lib/mix/tasks/doctor.ex 1 0 0 Yes N/A "
],
[
"100% 100% Mix.Tasks.Doctor.Explain lib/mix/tasks/doctor.explain.ex 1 0 0 Yes N/A "
],
[
"100% 100% Mix.Tasks.Doctor.Gen.Config lib/mix/tasks/doctor.gen.config.ex 1 0 0 Yes N/A "
],
[
"100% 0% Doctor.ModuleInformation lib/module_information.ex 4 0 4 Yes Yes "
],
[
"100% 0% Doctor.ModuleReport lib/module_report.ex 1 0 1 Yes Yes "
],
[
"100% 9% Doctor.ReportUtils lib/report_utils.ex 11 0 10 Yes N/A "
],
[
"N/A N/A Doctor.Reporter lib/reporter.ex 0 0 0 Yes N/A "
],
[
"100% 100% Doctor.Reporters.Full lib/reporters/full.ex 1 0 0 Yes N/A "
],
[
"100% 0% Doctor.Reporters.ModuleExplain lib/reporters/module_explain.ex 1 0 1 Yes N/A "
],
[
"100% 0% Doctor.Reporters.OutputUtils lib/reporters/output_utils.ex 6 0 6 Yes N/A "
],
[
"100% 100% Doctor.Reporters.Short lib/reporters/short.ex 1 0 0 Yes N/A "
],
[
"100% 100% Doctor.Reporters.Summary lib/reporters/summary.ex 1 0 0 Yes N/A "
],
[
"100% 0% Doctor.Specs lib/specs.ex 1 0 1 Yes Yes "
],
[
"---------------------------------------------------------------------------------------------------------------------------------------------------------------------------"
],
["Summary:\n"],
["Passed Modules: 17"],
["Failed Modules: 0"],
["Total Doc Coverage: 100.0%"],
["Total Moduledoc Coverage: 100.0%"],
["Total Spec Coverage: 23.7%\n"],
["Doctor validation has passed!"]
]
end
test "should output the summary report along with an error when an invalid doctor file path is provided" do
Mix.Tasks.Doctor.run(["--summary", "--config-file", "./not_a_real_file.exs"])
remove_at_exit_hook()
[[first_line] | rest_doctor_output] = get_shell_output()
assert first_line =~ "Doctor file not found at path"
assert first_line =~ "not_a_real_file.exs"
assert rest_doctor_output == [
["---------------------------------------------"],
["Summary:\n"],
["Passed Modules: 28"],
["Failed Modules: 8"],
["Total Doc Coverage: 82.9%"],
["Total Moduledoc Coverage: 76.5%"],
["Total Spec Coverage: 42.1%\n"],
["\e[31mDoctor validation has failed!\e[0m"]
]
end
test "should not report exceptions missing docs if `exception_moduledoc_required` is set to `false`" do
Mix.Tasks.Doctor.run(["--summary", "--config-file", "./test/configs/exceptions_moduledoc_not_required.exs"])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
["---------------------------------------------"],
["Summary:\n"],
["Passed Modules: 28"],
["Failed Modules: 8"],
["Total Doc Coverage: 82.9%"],
["Total Moduledoc Coverage: 76.5%"],
["Total Spec Coverage: 42.1%\n"],
["\e[31mDoctor validation has failed!\e[0m"]
]
end
test "should output the failed modules and the summary report when --failed is provided" do
Mix.Tasks.Doctor.run([
"--short",
"--failed",
"--config-file",
"./test/configs/exceptions_moduledoc_not_required.exs"
])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
["----------------------------------------------------------------------------------------------"],
["Doc Cov Spec Cov Functions Module Module Doc Struct Spec"],
[
"\e[31mN/A N/A 0 Doctor.AnotherBehaviourModule.Behaviour No N/A \e[0m"
],
[
"\e[31m100% 100% 1 Doctor.AnotherBehaviourModule No N/A \e[0m"
],
[
"\e[31m0% 0% 7 Doctor.NoDocs No N/A \e[0m"
],
[
"\e[31mN/A N/A 0 Doctor.NoStructSpecModule No No \e[0m"
],
[
"\e[31mN/A N/A 0 Doctor.OpaqueStructSpecModule No Yes \e[0m"
],
[
"\e[31m57% 57% 7 Doctor.PartialDocs No N/A \e[0m"
],
[
"\e[31mN/A N/A 0 Doctor.StructSpecModule No Yes \e[0m"
],
[
"\e[31m50% 50% 4 Doctor.UseModule Yes N/A \e[0m"
],
["----------------------------------------------------------------------------------------------"],
["Summary:\n"],
["Passed Modules: 28"],
["Failed Modules: 8"],
["Total Doc Coverage: 82.9%"],
["Total Moduledoc Coverage: 76.5%"],
["Total Spec Coverage: 42.1%\n"],
["\e[31mDoctor validation has failed!\e[0m"]
]
end
test "should output the summary report when a doctor file path is provided" do
Mix.Tasks.Doctor.run(["--summary", "--config-file", "./.doctor.exs"])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
["---------------------------------------------"],
["Summary:\n"],
["Passed Modules: 17"],
["Failed Modules: 0"],
["Total Doc Coverage: 100.0%"],
["Total Moduledoc Coverage: 100.0%"],
["Total Spec Coverage: 23.7%\n"],
["Doctor validation has passed!"]
]
end
test "should output the summary report with the correct output if given the --summary flag" do
Mix.Tasks.Doctor.run(["--summary"])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
["---------------------------------------------"],
["Summary:\n"],
["Passed Modules: 17"],
["Failed Modules: 0"],
["Total Doc Coverage: 100.0%"],
["Total Moduledoc Coverage: 100.0%"],
["Total Spec Coverage: 23.7%\n"],
["Doctor validation has passed!"]
]
end
test "should output the short report with the correct output if given the --short flag" do
Mix.Tasks.Doctor.run(["--short"])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
["----------------------------------------------------------------------------------------------"],
["Doc Cov Spec Cov Functions Module Module Doc Struct Spec"],
["100% 0% 3 Doctor.CLI Yes N/A "],
["100% 50% 4 Doctor.Config Yes Yes "],
["100% 0% 1 Doctor.Docs Yes Yes "],
["N/A N/A 0 Doctor Yes N/A "],
["100% 100% 1 Mix.Tasks.Doctor Yes N/A "],
["100% 100% 1 Mix.Tasks.Doctor.Explain Yes N/A "],
["100% 100% 1 Mix.Tasks.Doctor.Gen.Config Yes N/A "],
["100% 0% 4 Doctor.ModuleInformation Yes Yes "],
["100% 0% 1 Doctor.ModuleReport Yes Yes "],
["100% 9% 11 Doctor.ReportUtils Yes N/A "],
["N/A N/A 0 Doctor.Reporter Yes N/A "],
["100% 100% 1 Doctor.Reporters.Full Yes N/A "],
["100% 0% 1 Doctor.Reporters.ModuleExplain Yes N/A "],
["100% 0% 6 Doctor.Reporters.OutputUtils Yes N/A "],
["100% 100% 1 Doctor.Reporters.Short Yes N/A "],
["100% 100% 1 Doctor.Reporters.Summary Yes N/A "],
["100% 0% 1 Doctor.Specs Yes Yes "],
["----------------------------------------------------------------------------------------------"],
["Summary:\n"],
["Passed Modules: 17"],
["Failed Modules: 0"],
["Total Doc Coverage: 100.0%"],
["Total Moduledoc Coverage: 100.0%"],
["Total Spec Coverage: 23.7%\n"],
["Doctor validation has passed!"]
]
end
test "should output the full report with the correct output if given the --full flag" do
Mix.Tasks.Doctor.run(["--full"])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
[
"---------------------------------------------------------------------------------------------------------------------------------------------------------------------------"
],
[
"Doc Cov Spec Cov Module File Functions No Docs No Specs Module Doc Struct Spec"
],
[
"100% 0% Doctor.CLI lib/cli/cli.ex 3 0 3 Yes N/A "
],
[
"100% 50% Doctor.Config lib/config.ex 4 0 2 Yes Yes "
],
[
"100% 0% Doctor.Docs lib/docs.ex 1 0 1 Yes Yes "
],
[
"N/A N/A Doctor lib/doctor.ex 0 0 0 Yes N/A "
],
[
"100% 100% Mix.Tasks.Doctor lib/mix/tasks/doctor.ex 1 0 0 Yes N/A "
],
[
"100% 100% Mix.Tasks.Doctor.Explain lib/mix/tasks/doctor.explain.ex 1 0 0 Yes N/A "
],
[
"100% 100% Mix.Tasks.Doctor.Gen.Config lib/mix/tasks/doctor.gen.config.ex 1 0 0 Yes N/A "
],
[
"100% 0% Doctor.ModuleInformation lib/module_information.ex 4 0 4 Yes Yes "
],
[
"100% 0% Doctor.ModuleReport lib/module_report.ex 1 0 1 Yes Yes "
],
[
"100% 9% Doctor.ReportUtils lib/report_utils.ex 11 0 10 Yes N/A "
],
[
"N/A N/A Doctor.Reporter lib/reporter.ex 0 0 0 Yes N/A "
],
[
"100% 100% Doctor.Reporters.Full lib/reporters/full.ex 1 0 0 Yes N/A "
],
[
"100% 0% Doctor.Reporters.ModuleExplain lib/reporters/module_explain.ex 1 0 1 Yes N/A "
],
[
"100% 0% Doctor.Reporters.OutputUtils lib/reporters/output_utils.ex 6 0 6 Yes N/A "
],
[
"100% 100% Doctor.Reporters.Short lib/reporters/short.ex 1 0 0 Yes N/A "
],
[
"100% 100% Doctor.Reporters.Summary lib/reporters/summary.ex 1 0 0 Yes N/A "
],
[
"100% 0% Doctor.Specs lib/specs.ex 1 0 1 Yes Yes "
],
[
"---------------------------------------------------------------------------------------------------------------------------------------------------------------------------"
],
["Summary:\n"],
["Passed Modules: 17"],
["Failed Modules: 0"],
["Total Doc Coverage: 100.0%"],
["Total Moduledoc Coverage: 100.0%"],
["Total Spec Coverage: 23.7%\n"],
["Doctor validation has passed!"]
]
end
end
describe "mix doctor.explain" do
test "exception module with missing doc if `exception_moduledoc_required` is set to `true`" do
Mix.Tasks.Doctor.Explain.run([
"--config-file",
"./test/configs/exceptions_moduledoc_required.exs",
"Doctor.Exception"
])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
["\nFunction @doc @spec "],
["-----------------------------"],
["exception/1 ✓ ✓ "],
["\nModule Results:"],
["\e[32m Doc Coverage: 100.0%\e[0m"],
["\e[32m Spec Coverage: 100.0%\e[0m"],
["\e[31m Has Module Doc: ✗ --> Your config has an 'exception_moduledoc_required' value of true\e[0m"],
["\e[32m Has Struct Spec: N/A\e[0m"]
]
end
test "exception module with missing doc if `exception_moduledoc_required` is set to `false`" do
Mix.Tasks.Doctor.Explain.run([
"--config-file",
"./test/configs/exceptions_moduledoc_not_required.exs",
"Doctor.Exception"
])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
["\nFunction @doc @spec "],
["-----------------------------"],
["exception/1 ✓ ✓ "],
["\nModule Results:"],
["\e[32m Doc Coverage: 100.0%\e[0m"],
["\e[32m Spec Coverage: 100.0%\e[0m"],
["\e[32m Has Module Doc: ✗\e[0m"],
["\e[32m Has Struct Spec: N/A\e[0m"]
]
end
test "module with using macro and various inline functions" do
Mix.Tasks.Doctor.Explain.run([
"--config-file",
"./test/configs/exceptions_moduledoc_not_required.exs",
"Doctor.UseModule"
])
remove_at_exit_hook()
doctor_output = get_shell_output()
assert doctor_output == [
["Doctor file found. Loading configuration."],
["\nFunction @doc @spec "],
["----------------------------------------"],
["fun_without_spec_and_doc/0 ✗ ✗ "],
["fun_with_spec/0 ✗ ✓ "],
["fun_with_doc/0 ✓ ✗ "],
["fun_with_doc_and_spec/0 ✓ ✓ "],
["\nModule Results:"],
["\e[31m Doc Coverage: 50.0% --> Your config has a 'min_module_doc_coverage' value of 80\e[0m"],
["\e[32m Spec Coverage: 50.0%\e[0m"],
["\e[32m Has Module Doc: ✓\e[0m"],
["\e[32m Has Struct Spec: N/A\e[0m"]
]
end
end
defp get_shell_output() do
{:messages, message_mailbox} = Process.info(self(), :messages)
Enum.map(message_mailbox, fn
{:mix_shell, :info, message} -> message
{:mix_shell, :error, message} -> message
end)
end
defp remove_at_exit_hook() do
at_exit_hooks = :elixir_config.get(:at_exit)
filtered_hooks =
Enum.reject(at_exit_hooks, fn hook ->
function_info = Function.info(hook)
Keyword.get(function_info, :module) in [Mix.Tasks.Doctor, Mix.Tasks.Doctor.Explain]
end)
:elixir_config.put(:at_exit, filtered_hooks)
end
end
================================================
FILE: test/module_information_test.exs
================================================
defmodule Doctor.ModuleInformationTest do
use ExUnit.Case
alias Doctor.{ModuleInformation, ModuleReport}
describe "build/2" do
test "should find all of the docs for a module where all docs are present" do
full_func_list = [:func_1, :func_2, :func_3, :func_4, :func_5, :func_5, :func_6]
module_information =
Doctor.AllDocs
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.AllDocs)
docs =
module_information.docs
|> Enum.map(fn func_doc ->
func_doc.name
end)
|> Enum.sort()
specs =
module_information.specs
|> Enum.map(fn func_spec ->
func_spec.name
end)
|> Enum.sort()
assert is_map(module_information.module_doc)
assert module_information.file_ast == nil
assert module_information.file_relative_path == "test/sample_files/all_docs.ex"
assert specs == full_func_list
assert docs == full_func_list
end
test "should report behaviour functions properly" do
module_report =
Doctor.AnotherBehaviourModule
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.AnotherBehaviourModule)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.missed_specs == 0
assert module_report.missed_docs == 0
end
end
describe "load_user_defined_functions/1" do
test "should load user defined functions from AST" do
module_information =
Doctor.AllDocs
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.AllDocs)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
assert module_information != nil
assert Enum.sort(module_information.user_defined_functions) == [
{:func_1, 1, :none},
{:func_2, 1, :none},
{:func_3, 1, :none},
{:func_4, 1, :none},
{:func_5, 2, :none},
{:func_5, 3, :none},
{:func_6, 1, :none}
]
end
test "parent of nested module should ignore functions from nested modules" do
module_information =
Doctor.ParentModule
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.ParentModule)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
assert module_information.user_defined_functions == [{:outer, 0, :none}]
end
test "nested module should include its functions" do
module_information =
Doctor.ParentModule.Nested
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.ParentModule.Nested)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
assert module_information.user_defined_functions == [{:inner, 0, :none}]
end
end
end
================================================
FILE: test/module_report_test.exs
================================================
defmodule Doctor.ModuleReportTest do
use ExUnit.Case
alias Doctor.{ModuleInformation, ModuleReport}
test "build/1 should build the correct report struct for a file with full coverage" do
module_report =
Doctor.AllDocs
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.AllDocs)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 7
assert module_report.has_module_doc
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Doctor.AllDocs"
assert module_report.doc_coverage == Decimal.new("100")
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report struct for a file with partial coverage" do
module_report =
Doctor.PartialDocs
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.PartialDocs)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 7
refute module_report.has_module_doc
assert module_report.missed_docs == 3
assert module_report.missed_specs == 3
assert module_report.module == "Doctor.PartialDocs"
assert module_report.doc_coverage == Decimal.new("57.14285714285714285714285714")
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report struct for a file that implements behaviour callbacks" do
module_report =
Doctor.BehaviourModule
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.BehaviourModule)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 3
assert module_report.has_module_doc
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Doctor.BehaviourModule"
assert module_report.doc_coverage == Decimal.new("100")
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report struct for a file that implements behaviour callbacks with multiple clauses" do
module_report =
Doctor.FooBar
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.FooBar)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 6
assert module_report.has_module_doc
assert module_report.missed_docs == 1
assert module_report.missed_specs == 3
assert module_report.module == "Doctor.FooBar"
assert module_report.doc_coverage == Decimal.new("83.33333333333333333333333333")
assert module_report.spec_coverage == Decimal.new("50.0")
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report struct for a file with no coverage" do
module_report =
Doctor.NoDocs
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.NoDocs)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 7
refute module_report.has_module_doc
assert module_report.missed_docs == 7
assert module_report.missed_specs == 7
assert module_report.module == "Doctor.NoDocs"
assert module_report.doc_coverage == Decimal.new("0")
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report struct for a file with struct specs" do
module_report =
Doctor.StructSpecModule
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.StructSpecModule)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 0
refute module_report.has_module_doc
assert module_report.has_struct_type_spec
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Doctor.StructSpecModule"
assert module_report.doc_coverage == nil
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report struct for a file with no struct specs" do
module_report =
Doctor.NoStructSpecModule
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.NoStructSpecModule)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 0
refute module_report.has_module_doc
refute module_report.has_struct_type_spec
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Doctor.NoStructSpecModule"
assert module_report.doc_coverage == nil
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report struct for a file with an opaque struct spec" do
module_report =
Doctor.OpaqueStructSpecModule
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.OpaqueStructSpecModule)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 0
refute module_report.has_module_doc
assert module_report.has_struct_type_spec
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Doctor.OpaqueStructSpecModule"
assert module_report.doc_coverage == nil
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report for an exception" do
module_report =
Doctor.Exception
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.Exception)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 1
refute module_report.has_module_doc
assert module_report.has_struct_type_spec
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Doctor.Exception"
assert module_report.doc_coverage == Decimal.new("100")
assert module_report.properties == [is_exception: true, is_protocol_implementation: false]
end
test "build/1 should build the correct report for a module with __using__ macro" do
module_report =
Doctor.UseModule
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.UseModule)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 4
assert module_report.has_module_doc
assert module_report.has_struct_type_spec == :not_struct
assert module_report.missed_docs == 2
assert module_report.missed_specs == 2
assert module_report.module == "Doctor.UseModule"
assert module_report.doc_coverage == Decimal.new("50.0")
assert module_report.spec_coverage == Decimal.new("50.0")
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report for a module with a nested module" do
module_report =
Doctor.ParentModule
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.ParentModule)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 1
assert module_report.has_module_doc
assert module_report.has_struct_type_spec == :not_struct
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Doctor.ParentModule"
assert module_report.doc_coverage == Decimal.new("100")
assert module_report.spec_coverage == Decimal.new("100")
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report for a nested module" do
module_report =
Doctor.ParentModule.Nested
|> Code.fetch_docs()
|> ModuleInformation.build(Doctor.ParentModule.Nested)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.functions == 1
assert module_report.has_module_doc
assert module_report.has_struct_type_spec == :not_struct
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Doctor.ParentModule.Nested"
assert module_report.doc_coverage == Decimal.new("100")
assert module_report.spec_coverage == Decimal.new("100")
assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
end
test "build/1 should build the correct report for a protocol derivation" do
module_report =
Inspect.Doctor.DeriveProtocol
|> Code.fetch_docs()
|> ModuleInformation.build(Inspect.Doctor.DeriveProtocol)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.is_protocol_implementation == true
assert module_report.functions == 0
assert module_report.has_module_doc == true
assert module_report.has_struct_type_spec == :not_struct
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Inspect.Doctor.DeriveProtocol"
assert module_report.doc_coverage == nil
assert module_report.spec_coverage == nil
assert module_report.properties == [is_exception: false, is_protocol_implementation: true]
end
test "build/1 should build the correct report for a protocol implementation" do
module_report =
Inspect.Doctor.ImplementProtocol
|> Code.fetch_docs()
|> ModuleInformation.build(Inspect.Doctor.ImplementProtocol)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
assert module_report.is_protocol_implementation == true
assert module_report.functions == 0
assert module_report.has_module_doc == true
assert module_report.has_struct_type_spec == :not_struct
assert module_report.missed_docs == 0
assert module_report.missed_specs == 0
assert module_report.module == "Inspect.Doctor.ImplementProtocol"
assert module_report.doc_coverage == nil
assert module_report.spec_coverage == nil
assert module_report.properties == [is_exception: false, is_protocol_implementation: true]
end
end
================================================
FILE: test/report_utils_test.exs
================================================
defmodule Doctor.ReportUtilsTest do
use ExUnit.Case
import ExUnit.CaptureLog
alias Doctor.{ModuleInformation, ModuleReport, ReportUtils}
setup do
reports =
[Doctor.AllDocs, Doctor.PartialDocs, Doctor.NoDocs]
|> Enum.map(fn module ->
report =
module
|> Code.fetch_docs()
|> ModuleInformation.build(module)
|> ModuleInformation.load_file_ast()
|> ModuleInformation.load_user_defined_functions()
|> ModuleReport.build()
{module, report}
end)
|> Map.new()
%{reports: reports}
end
test "count_total_functions/1 should return the correct number of functions across a list of module reports",
%{reports: reports} do
assert reports
|> Map.values()
|> ReportUtils.count_total_functions() == 21
end
test "count_total_documented_functions/1 should return the correct number of documented functions across a list of module reports",
%{reports: reports} do
assert reports
|> Map.values()
|> ReportUtils.count_total_documented_functions() == 11
end
test "count_total_speced_functions/1 should return the correct number of speced functions across a list of module reports",
%{reports: reports} do
assert reports
|> Map.values()
|> ReportUtils.count_total_speced_functions() == 11
end
test "count_total_passed_modules/1 should return the correct number of failed modules from a list of module reports if moduledoc config true",
%{reports: reports} do
config = %Doctor.Config{min_overall_moduledoc_coverage: 100}
assert reports
|> Map.values()
|> ReportUtils.count_total_passed_modules(config) == 1
end
test "count_total_passed_modules/1 should return the correct number of failed modules from a list of module reports if config threshold set low",
%{reports: reports} do
config = %Doctor.Config{min_overall_doc_coverage: 20, min_overall_moduledoc_coverage: 100}
assert reports
|> Map.values()
|> ReportUtils.count_total_passed_modules(config) == 1
end
test "count_total_failed_modules/1 should return the correct number of failed modules from a list of module reports if moduledoc config true",
%{reports: reports} do
config = %Doctor.Config{min_overall_moduledoc_coverage: 100}
assert reports
|> Map.values()
|> ReportUtils.count_total_failed_modules(config) == 2
end
test "count_total_failed_modules/1 should return the correct number of failed modules from a list of module reports if config threshold set low",
%{reports: reports} do
config = %Doctor.Config{min_overall_doc_coverage: 20, min_overall_moduledoc_coverage: 100}
assert reports
|> Map.values()
|> ReportUtils.count_total_failed_modules(config) == 2
end
test "calc_overall_doc_coverage/1 should return the correct percentage a list of module reports",
%{reports: reports} do
assert reports
|> Map.values()
|> ReportUtils.calc_overall_doc_coverage() ==
Decimal.new("52.38095238095238095238095238")
end
test "calc_overall_spec_coverage/1 should return the correct percentage a list of module reports",
%{reports: reports} do
assert reports
|> Map.values()
|> ReportUtils.calc_overall_spec_coverage() ==
Decimal.new("52.38095238095238095238095238")
end
test "doctor_report_passed?/2 should return false if the report fails given required moduledocs",
%{
reports: reports
} do
config = %Doctor.Config{min_overall_moduledoc_coverage: 100}
refute reports
|> Map.values()
|> ReportUtils.doctor_report_passed?(config)
end
test "doctor_report_passed?/2 should return false if the report fails given high threshold", %{
reports: reports
} do
config = %Doctor.Config{
min_module_doc_coverage: 0,
min_overall_moduledoc_coverage: 0,
min_overall_doc_coverage: 80
}
refute reports
|> Map.values()
|> ReportUtils.doctor_report_passed?(config)
end
test "doctor_report_passed?/2 should return false if the report fails given low threshold", %{
reports: reports
} do
{config, warn_msg} =
with_log(fn ->
Doctor.Config.new(
moduledoc_required: false,
min_module_doc_coverage: 0,
min_overall_doc_coverage: 50
)
end)
assert warn_msg =~ ":moduledoc_required in .doctor.exs is a deprecated option."
assert reports
|> Map.values()
|> ReportUtils.doctor_report_passed?(config)
end
end
================================================
FILE: test/sample_files/all_docs.ex
================================================
defmodule Doctor.AllDocs do
@moduledoc "This is a module doc"
@spec func_1(integer()) :: integer()
@doc "Function doc 1"
def func_1(input) do
input + 1
end
@spec func_2(integer()) :: integer()
@doc """
Function doc 2
"""
def func_2(input), do: input + 2
@spec func_3(integer()) :: integer()
@doc "Function doc 3"
def func_3(input) when is_integer(input) do
input + 3
end
@spec func_4(integer()) :: integer()
@doc "Function doc 4"
def func_4(input) when is_integer(input), do: input + 4
@spec func_5(integer(), integer()) :: integer()
@doc "Function doc 5 with 2 args"
def func_5(input_1, input_2) do
func_5(input_1, input_2, 5)
end
@spec func_5(integer(), integer(), integer()) :: integer()
@doc "Function doc 5 with 3 args"
def func_5(input_1, input_2, input_3) do
input_1 + input_2 + input_3
end
@spec func_6(String.t()) :: String.t()
@doc "Function doc 6"
def func_6("match" = input), do: input
def func_6("matches" = input), do: input
def func_6("matcher" = input), do: input
def func_6("matching" = input), do: input
def func_6(_), do: "no match"
end
================================================
FILE: test/sample_files/another_behaviour_module.ex
================================================
defmodule Doctor.AnotherBehaviourModule.Behaviour do
@callback func() :: String.t()
end
defmodule Doctor.AnotherBehaviourModule do
@behaviour Doctor.AnotherBehaviourModule.Behaviour
@impl Doctor.AnotherBehaviourModule.Behaviour
def func, do: "Hello world"
end
================================================
FILE: test/sample_files/behaviour_module.ex
================================================
defmodule Doctor.BehaviourModule do
@moduledoc """
This is a GenServer module that has 100% code coverage
"""
use GenServer
@impl true
def init(stack) do
{:ok, stack}
end
@impl GenServer
@doc "Something or other"
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
def handle_call(:nop, _from, state) do
{:reply, state}
end
@impl true
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
end
================================================
FILE: test/sample_files/custom_behaviour_module.ex
================================================
defmodule Doctor.FooBarBehaviour do
@moduledoc """
A custom behaviour module
"""
@doc """
The famous foo function
"""
@callback foo(mode :: atom()) :: integer()
@doc """
And the infamous bar function
"""
@callback bar(mode :: atom()) :: integer()
@callback bar(mode :: atom(), param :: integer()) :: integer()
end
defmodule Doctor.FooBar do
@moduledoc """
Implementation of the FooBarBehaviour
"""
@behaviour Doctor.FooBarBehaviour
def foo(:five), do: 5
# This should not
@impl true
def foo(:one), do: 1
# neither this
def foo(:two), do: 2
@impl Doctor.FooBarBehaviour
def bar(:one), do: 1
def bar(:two), do: 2
def bar(:three), do: 3
# This should raise both a missing spec and a missing doc
def bar(:test, value), do: value
# This should pass
@impl Doctor.FooBarBehaviour
def bar(:bar, value), do: value
# This should raise both a missing doc and spec
def bar(:noop, _value1, _value2), do: 0
end
================================================
FILE: test/sample_files/derive_protocol.ex
================================================
defmodule Doctor.DeriveProtocol do
@moduledoc """
An Example Derivation of a Protocol
"""
@derive Inspect
defstruct [:foo, :bar]
@type t :: %__MODULE__{foo: String.t(), bar: non_neg_integer}
end
================================================
FILE: test/sample_files/exception.ex
================================================
defmodule Doctor.Exception do
defexception [:message]
@impl true
def exception(value) do
msg = "doctor exception: #{inspect(value)}"
%Doctor.Exception{message: msg}
end
end
================================================
FILE: test/sample_files/implement_protocol.ex
================================================
defmodule Doctor.ImplementProtocol do
@moduledoc """
An Example Implementation of a Protocol
"""
defstruct [:foo, :bar]
@type t :: %__MODULE__{foo: String.t(), bar: non_neg_integer}
defimpl Inspect do
import Inspect.Algebra
def inspect(struct, opts) do
doc = struct |> Map.from_struct() |> Map.to_list() |> to_doc(opts)
concat(["#ExampleDefImpl<", doc, ">"])
end
end
end
================================================
FILE: test/sample_files/nested_module.ex
================================================
defmodule Doctor.ParentModule do
@moduledoc """
A module containing another module
"""
defmodule Nested do
@moduledoc """
A nested module
"""
@doc """
A function in the nested module
"""
@spec inner :: :ok
def inner, do: :ok
end
@doc """
A function in the outer module
"""
@spec outer :: :ok
def outer, do: :ok
end
================================================
FILE: test/sample_files/no_docs.ex
================================================
defmodule Doctor.NoDocs do
def func_1(input) do
input + 1
end
def func_2(input), do: input + 2
def func_3(input) when is_integer(input) do
input + 3
end
def func_4(input) when is_integer(input), do: input + 4
def func_5(input_1, input_2) do
func_5(input_1, input_2, 5)
end
def func_5(input_1, input_2, input_3) do
input_1 + input_2 + input_3
end
def func_6("match" = input), do: input
def func_6("matches" = input), do: input
def func_6("matcher" = input), do: input
def func_6("matching" = input), do: input
def func_6(_), do: "no match"
end
================================================
FILE: test/sample_files/no_struct_spec_module.ex
================================================
defmodule Doctor.NoStructSpecModule do
defstruct ~w(name arity)a
end
================================================
FILE: test/sample_files/opaque_struct_spec_module.ex
================================================
defmodule Doctor.OpaqueStructSpecModule do
defstruct ~w(name arity)a
@opaque t :: %__MODULE__{}
end
================================================
FILE: test/sample_files/partial_docs.ex
================================================
defmodule Doctor.PartialDocs do
def func_1(input) do
input + 1
end
def func_2(input), do: input + 2
def func_3(input) when is_integer(input) do
input + 3
end
@spec func_4(integer()) :: integer()
@doc "Function doc 4"
def func_4(input) when is_integer(input), do: input + 4
@spec func_5(integer(), integer()) :: integer()
@doc "Function doc 5 with 2 args"
def func_5(input_1, input_2) do
func_5(input_1, input_2, 5)
end
@spec func_5(integer(), integer(), integer()) :: integer()
@doc "Function doc 5 with 3 args"
def func_5(input_1, input_2, input_3) do
input_1 + input_2 + input_3
end
@spec func_6(String.t()) :: String.t()
@doc "Function doc 6"
def func_6("match" = input), do: input
def func_6("matches" = input), do: input
def func_6("matcher" = input), do: input
def func_6("matching" = input), do: input
def func_6(_), do: "no match"
end
================================================
FILE: test/sample_files/struct_spec_module.ex
================================================
defmodule Doctor.StructSpecModule do
@type t :: %__MODULE__{
name: atom(),
arity: integer()
}
defstruct ~w(name arity)a
end
================================================
FILE: test/sample_files/use_module.ex
================================================
defmodule Doctor.UseModule do
@moduledoc """
A module with __using__ macro
"""
defmacro __using__(_opts) do
quote do
@doc """
Returns :ok
"""
@spec fun_with_doc_and_spec() :: :ok
def fun_with_doc_and_spec, do: :ok
@doc """
Sample function
"""
def fun_with_doc, do: :ok
@spec fun_with_spec() :: :ok
def fun_with_spec, do: :ok
def fun_without_spec_and_doc, do: :ok
end
end
end
================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()
gitextract_0by5ihkr/
├── .doctor.exs
├── .formatter.exs
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── master.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config/
│ └── config.exs
├── coveralls.json
├── lib/
│ ├── cli/
│ │ └── cli.ex
│ ├── config.ex
│ ├── docs.ex
│ ├── doctor.ex
│ ├── mix/
│ │ └── tasks/
│ │ ├── doctor.ex
│ │ ├── doctor.explain.ex
│ │ └── doctor.gen.config.ex
│ ├── module_information.ex
│ ├── module_report.ex
│ ├── report_utils.ex
│ ├── reporter.ex
│ ├── reporters/
│ │ ├── full.ex
│ │ ├── module_explain.ex
│ │ ├── output_utils.ex
│ │ ├── short.ex
│ │ └── summary.ex
│ └── specs.ex
├── mix.exs
└── test/
├── config_test.exs
├── configs/
│ ├── exceptions_moduledoc_not_required.exs
│ └── exceptions_moduledoc_required.exs
├── mix_doctor_test.exs
├── module_information_test.exs
├── module_report_test.exs
├── report_utils_test.exs
├── sample_files/
│ ├── all_docs.ex
│ ├── another_behaviour_module.ex
│ ├── behaviour_module.ex
│ ├── custom_behaviour_module.ex
│ ├── derive_protocol.ex
│ ├── exception.ex
│ ├── implement_protocol.ex
│ ├── nested_module.ex
│ ├── no_docs.ex
│ ├── no_struct_spec_module.ex
│ ├── opaque_struct_spec_module.ex
│ ├── partial_docs.ex
│ ├── struct_spec_module.ex
│ └── use_module.ex
└── test_helper.exs
SYMBOL INDEX (245 symbols across 37 files)
FILE: lib/cli/cli.ex
class Doctor.CLI (line 1) | defmodule Doctor.CLI
method generate_module_report_list (line 13) | def generate_module_report_list(args) do
method generate_single_module_report (line 39) | def generate_single_module_report(module_name, args) do
method process_module_report_list (line 61) | def process_module_report_list(module_report_list, args) do
method generate_module_entry (line 67) | defp generate_module_entry(module) do
method async_fetch_user_defined_functions (line 73) | defp async_fetch_user_defined_functions(%ModuleInformation{} = module_...
method get_application_modules (line 81) | defp get_application_modules(application) do
method filter_ignore_paths (line 92) | defp filter_ignore_paths(file_relative_path, ignore_paths) do
method compare_ignore_path (line 99) | defp compare_ignore_path(file_relative_path, %Regex{} = ignore_pattern...
method compare_ignore_path (line 115) | defp compare_ignore_path(_, ignore_value),
method filter_ignore_modules (line 118) | defp filter_ignore_modules(module, ignore_modules) do
method compare_ignore_module (line 125) | defp compare_ignore_module(module, %Regex{} = ignore_pattern) do
method compare_ignore_module (line 146) | defp compare_ignore_module(_, ignore_value),
FILE: lib/config.ex
class Doctor.Config (line 1) | defmodule Doctor.Config
method new (line 58) | def new(attrs \\ %{}) do
method moduledoc_required? (line 73) | def moduledoc_required?(%{min_overall_moduledoc_coverage: 100}), do: true
method moduledoc_required? (line 74) | def moduledoc_required?(_), do: false
method config_defaults_as_string (line 79) | def config_defaults_as_string do
method config_file (line 92) | def config_file, do: @config_file
method interpret_moduledoc_required (line 98) | defp interpret_moduledoc_required(%{moduledoc_required: nil} = config) do
method interpret_moduledoc_required (line 102) | defp interpret_moduledoc_required(%{moduledoc_required: true} = config...
method interpret_moduledoc_required (line 107) | defp interpret_moduledoc_required(%{moduledoc_required: false} = confi...
method warn_deprecation (line 112) | defp warn_deprecation(_bool, val) do
FILE: lib/docs.ex
class Doctor.Docs (line 1) | defmodule Doctor.Docs
method build (line 21) | def build({{kind, name, arity}, _annotation, _signature, doc, _metadat...
FILE: lib/doctor.ex
class Doctor (line 1) | defmodule Doctor
FILE: lib/mix/tasks/doctor.ex
class Mix.Tasks.Doctor (line 1) | defmodule Mix.Tasks.Doctor
method run (line 48) | def run(args) do
method run_umbrella (line 67) | defp run_umbrella(config) do
method run_default (line 83) | defp run_default(config) do
method init_umbrella_acc (line 102) | defp init_umbrella_acc(config) do
method load_config_file (line 122) | defp load_config_file(%{config_file_path: file_path} = _cli_args) do
method load_config_file (line 138) | defp load_config_file(_) do
method parse_cli_args (line 161) | defp parse_cli_args(args) do
FILE: lib/mix/tasks/doctor.explain.ex
class Mix.Tasks.Doctor.Explain (line 1) | defmodule Mix.Tasks.Doctor.Explain
method run (line 61) | def run(args) do
method run_umbrella (line 90) | defp run_umbrella(module_name, config) do
method run_default (line 110) | defp run_default(module_name, config) do
method init_umbrella_acc (line 130) | defp init_umbrella_acc(module_name, config) do
method report_umbrella_result (line 142) | defp report_umbrella_result(%{found: false}, module_name, _config) do
method report_umbrella_result (line 146) | defp report_umbrella_result(%{result: true}, _module_name, _config), d...
method report_umbrella_result (line 148) | defp report_umbrella_result(_acc, _module_name, config) do
method load_config_file (line 156) | defp load_config_file(%{config_file_path: file_path} = _cli_args) do
method load_config_file (line 172) | defp load_config_file(_) do
method parse_cli_args (line 195) | defp parse_cli_args(args) do
FILE: lib/mix/tasks/doctor.gen.config.ex
class Mix.Tasks.Doctor.Gen.Config (line 1) | defmodule Mix.Tasks.Doctor.Gen.Config
method run (line 39) | def run(_args) do
method create_config_file (line 56) | defp create_config_file do
FILE: lib/module_information.ex
class Doctor.ModuleInformation (line 1) | defmodule Doctor.ModuleInformation
method build (line 45) | def build({docs_version, _annotation, _language, _format, module_doc, ...
method load_file_ast (line 72) | def load_file_ast(%ModuleInformation{} = module_info) do
method contains_struct_type_spec? (line 85) | def contains_struct_type_spec?(module) do
method is_struct? (line 104) | defp is_struct?(module) do
method load_user_defined_functions (line 119) | def load_user_defined_functions(%ModuleInformation{} = module_info) do
method load_using_docs_and_specs (line 141) | defp load_using_docs_and_specs(%ModuleInformation{} = module_info, mod...
method get_module_behaviours (line 165) | defp get_module_behaviours(module) do
method get_full_file_path (line 179) | defp get_full_file_path(module) do
method get_relative_file_path (line 186) | defp get_relative_file_path(module) do
method parse_ast_node_for_def (line 201) | defp parse_ast_node_for_def({:@, _line_number, [{:doc, _, [false]}]} =...
method parse_ast_node_for_def (line 207) | defp parse_ast_node_for_def({:@, _line_number, [{:impl, _, impl_def}]}...
method parse_ast_node_for_def (line 214) | defp parse_ast_node_for_def(
method parse_ast_node_for_def (line 225) | defp parse_ast_node_for_def(
method parse_ast_node_for_def (line 236) | defp parse_ast_node_for_def(
method parse_ast_node_for_def (line 247) | defp parse_ast_node_for_def(ast, acc) do
method unnest (line 256) | defp unnest(ast, acc) do
method update_acc_for_def (line 260) | defp update_acc_for_def(acc, function_name, function_arity, last_impl) do
method normalize_impl (line 284) | defp normalize_impl([{:__aliases__, _, module}]) do
method normalize_impl (line 288) | defp normalize_impl(value) do
method parse_ast_node_for_defmodules (line 308) | defp parse_ast_node_for_defmodules(ast, acc) do
method pop_module_stack (line 320) | defp pop_module_stack(ast, acc) do
method get_function_arity (line 324) | defp get_function_arity(nil), do: 0
method get_function_arity (line 325) | defp get_function_arity(args), do: length(args)
method parse_ast_for_using (line 327) | defp parse_ast_for_using({:defmacro, _macro_line, [{:__using__, _line,...
method parse_ast_for_using (line 330) | defp parse_ast_for_using(ast, acc), do: {ast, acc}
method parse_ast_using_node (line 332) | defp parse_ast_using_node(
method parse_ast_using_node (line 338) | defp parse_ast_using_node(
method parse_ast_using_node (line 344) | defp parse_ast_using_node(
method parse_ast_using_node (line 351) | defp parse_ast_using_node(
method parse_ast_using_node (line 358) | defp parse_ast_using_node(
method parse_ast_using_node (line 365) | defp parse_ast_using_node(ast, acc), do: {ast, acc}
method update_acc_for_using (line 367) | defp update_acc_for_using(function_name, args, acc) do
FILE: lib/module_report.ex
class Doctor.ModuleReport (line 1) | defmodule Doctor.ModuleReport
method build (line 43) | def build(%ModuleInformation{} = module_info) do
method generate_module_name (line 59) | defp generate_module_name(module) do
method calculate_missed_docs (line 65) | defp calculate_missed_docs(module_info) do
method calculate_doc_coverage (line 86) | defp calculate_doc_coverage(module_info) do
method calculate_missed_specs (line 99) | defp calculate_missed_specs(module_info) do
method calculate_spec_coverage (line 123) | defp calculate_spec_coverage(module_info) do
method has_module_doc? (line 136) | defp has_module_doc?(module_info) do
method is_protocol_implementation? (line 140) | defp is_protocol_implementation?(module_info) do
FILE: lib/report_utils.ex
class Doctor.ReportUtils (line 1) | defmodule Doctor.ReportUtils
method count_total_functions (line 11) | def count_total_functions(module_report_list) do
method count_total_documented_functions (line 21) | def count_total_documented_functions(module_report_list) do
method count_total_speced_functions (line 33) | def count_total_speced_functions(module_report_list) do
method count_total_passed_modules (line 45) | def count_total_passed_modules(module_report_list, %Config{} = config) do
method count_total_failed_modules (line 55) | def count_total_failed_modules(module_report_list, %Config{} = config) do
method calc_overall_doc_coverage (line 65) | def calc_overall_doc_coverage(module_report_list) do
method calc_overall_moduledoc_coverage (line 81) | def calc_overall_moduledoc_coverage(module_report_list) do
method calc_overall_spec_coverage (line 97) | def calc_overall_spec_coverage(module_report_list) do
method module_passed_validation? (line 113) | def module_passed_validation?(
method valid_module_doc? (line 133) | defp valid_module_doc?(%ModuleReport{is_protocol_implementation: true}...
method valid_module_doc? (line 137) | defp valid_module_doc?(%ModuleReport{properties: properties} = module_...
method doctor_report_passed? (line 154) | def doctor_report_passed?(module_report_list, config) do
method doctor_report_errors (line 162) | def doctor_report_errors(module_report_list, %Config{} = config) do
method calc_coverage_pass (line 206) | defp calc_coverage_pass(_coverage, _threshold), do: true
FILE: lib/reporter.ex
class Doctor.Reporter (line 1) | defmodule Doctor.Reporter
FILE: lib/reporters/full.ex
class Doctor.Reporters.Full (line 1) | defmodule Doctor.Reporters.Full
method generate_report (line 26) | def generate_report(module_reports, args) do
method print_header (line 75) | defp print_header() do
method print_divider (line 92) | defp print_divider do
method print_footer (line 98) | defp print_footer(errors, passed, failed, doc_coverage, moduledoc_cove...
method massage_coverage (line 132) | defp massage_coverage(coverage) do
method massage_module_doc (line 140) | defp massage_module_doc(%{is_protocol_implementation: true}), do: "N/A"
method massage_module_doc (line 141) | defp massage_module_doc(%{has_module_doc: true}), do: "Yes"
method massage_module_doc (line 142) | defp massage_module_doc(%{has_module_doc: false}), do: "No"
method massage_struct_type_spec (line 144) | defp massage_struct_type_spec(:not_struct), do: "N/A"
method massage_struct_type_spec (line 145) | defp massage_struct_type_spec(true), do: "Yes"
method massage_struct_type_spec (line 146) | defp massage_struct_type_spec(false), do: "No"
FILE: lib/reporters/module_explain.ex
class Doctor.Reporters.ModuleExplain (line 1) | defmodule Doctor.Reporters.ModuleExplain
method generate_report (line 23) | def generate_report(%ModuleInformation{} = module_information, %Config...
method valid_module? (line 82) | defp valid_module?(%ModuleReport{is_protocol_implementation: true}, _c...
method valid_module? (line 84) | defp valid_module?(module_report, config) do
method valid_struct_spec? (line 91) | defp valid_struct_spec?(module_report, config) do
method valid_moduledoc? (line 96) | defp valid_moduledoc?(%ModuleReport{is_protocol_implementation: true},...
method valid_moduledoc? (line 98) | defp valid_moduledoc?(module_report, config) do
method valid_doc_coverage? (line 103) | defp valid_doc_coverage?(%ModuleReport{is_protocol_implementation: tru...
method valid_doc_coverage? (line 105) | defp valid_doc_coverage?(module_report, config) do
method valid_spec_coverage? (line 109) | defp valid_spec_coverage?(module_report, config) do
method doc_coverage (line 113) | defp doc_coverage(module_report) do
method spec_coverage (line 119) | defp spec_coverage(module_report) do
method print_struct_spec (line 125) | defp print_struct_spec(%ModuleReport{} = module_report, %Config{} = co...
method print_module_doc (line 137) | defp print_module_doc(%ModuleReport{is_protocol_implementation: true},...
method print_module_doc (line 141) | defp print_module_doc(%ModuleReport{} = module_report, %Config{} = con...
method print_doc_coverage (line 160) | defp print_doc_coverage(%ModuleReport{is_protocol_implementation: true...
method print_doc_coverage (line 164) | defp print_doc_coverage(%ModuleReport{} = module_report, %Config{} = c...
method print_spec_coverage (line 176) | defp print_spec_coverage(%ModuleReport{is_protocol_implementation: tru...
method print_spec_coverage (line 180) | defp print_spec_coverage(%ModuleReport{} = module_report, %Config{} = ...
method generate_header (line 192) | defp generate_header(function_name_length) do
method has_doc (line 203) | defp has_doc(function, arity, :none, module_docs) do
method has_doc (line 213) | defp has_doc(_, _, _, _) do
method has_spec (line 217) | defp has_spec(function, arity, :none, module_specs) do
method has_spec (line 224) | defp has_spec(_, _, _, _) do
FILE: lib/reporters/output_utils.ex
class Doctor.Reporters.OutputUtils (line 1) | defmodule Doctor.Reporters.OutputUtils
method generate_table_line (line 13) | def generate_table_line(line_data) do
method print_divider (line 27) | def print_divider(length) do
method print_pass_or_fail (line 36) | def print_pass_or_fail(true), do: "\u2713"
method print_pass_or_fail (line 37) | def print_pass_or_fail(false), do: "\u2717"
method print_pass_or_fail (line 38) | def print_pass_or_fail(:not_struct), do: "N/A"
method print_error (line 43) | def print_error(string), do: Mix.shell().info(ANSI.red() <> string <> ...
method print_success (line 48) | def print_success(string), do: Mix.shell().info(ANSI.green() <> string...
method gen_fixed_width_string (line 53) | def gen_fixed_width_string(value, width, padding \\ 2)
method gen_fixed_width_string (line 67) | def gen_fixed_width_string(value, width, padding) do
FILE: lib/reporters/short.ex
class Doctor.Reporters.Short (line 1) | defmodule Doctor.Reporters.Short
method generate_report (line 23) | def generate_report(module_reports, args) do
method print_header (line 69) | defp print_header() do
method print_divider (line 83) | defp print_divider do
method print_footer (line 89) | defp print_footer(pass, passed, failed, doc_coverage, moduledoc_covera...
method massage_coverage (line 109) | defp massage_coverage(coverage) do
method massage_module_doc (line 117) | defp massage_module_doc(%{is_protocol_implementation: true}), do: "N/A"
method massage_module_doc (line 118) | defp massage_module_doc(%{has_module_doc: true}), do: "Yes"
method massage_module_doc (line 119) | defp massage_module_doc(%{has_module_doc: false}), do: "No"
method massage_struct_type_spec (line 121) | defp massage_struct_type_spec(:not_struct), do: "N/A"
method massage_struct_type_spec (line 122) | defp massage_struct_type_spec(true), do: "Yes"
method massage_struct_type_spec (line 123) | defp massage_struct_type_spec(false), do: "No"
FILE: lib/reporters/summary.ex
class Doctor.Reporters.Summary (line 1) | defmodule Doctor.Reporters.Summary
method generate_report (line 16) | def generate_report(module_reports, args) do
method print_divider (line 34) | defp print_divider do
method print_footer (line 40) | defp print_footer(pass, passed, failed, doc_coverage, moduledoc_covera...
FILE: lib/specs.ex
class Doctor.Specs (line 1) | defmodule Doctor.Specs
method build (line 19) | def build({{name, arity}, _spec}) do
FILE: mix.exs
class Doctor.MixProject (line 1) | defmodule Doctor.MixProject
method project (line 6) | def project do
method application (line 34) | def application do
method elixirc_paths (line 40) | defp elixirc_paths(:test), do: ["lib", "test/sample_files"]
method elixirc_paths (line 41) | defp elixirc_paths(_), do: ["lib"]
method package (line 43) | defp package() do
method deps (line 56) | defp deps do
FILE: test/config_test.exs
class Doctor.ConfigTest (line 1) | defmodule Doctor.ConfigTest
FILE: test/mix_doctor_test.exs
class Mix.Tasks.DoctorTest (line 1) | defmodule Mix.Tasks.DoctorTest
method get_shell_output (line 404) | defp get_shell_output() do
method remove_at_exit_hook (line 413) | defp remove_at_exit_hook() do
FILE: test/module_information_test.exs
class Doctor.ModuleInformationTest (line 1) | defmodule Doctor.ModuleInformationTest
FILE: test/module_report_test.exs
class Doctor.ModuleReportTest (line 1) | defmodule Doctor.ModuleReportTest
FILE: test/report_utils_test.exs
class Doctor.ReportUtilsTest (line 1) | defmodule Doctor.ReportUtilsTest
FILE: test/sample_files/all_docs.ex
class Doctor.AllDocs (line 1) | defmodule Doctor.AllDocs
method func_1 (line 6) | def func_1(input) do
method func_2 (line 14) | def func_2(input), do: input + 2
method func_5 (line 28) | def func_5(input_1, input_2) do
method func_5 (line 34) | def func_5(input_1, input_2, input_3) do
method func_6 (line 40) | def func_6("match" = input), do: input
method func_6 (line 41) | def func_6("matches" = input), do: input
method func_6 (line 42) | def func_6("matcher" = input), do: input
method func_6 (line 43) | def func_6("matching" = input), do: input
method func_6 (line 44) | def func_6(_), do: "no match"
FILE: test/sample_files/another_behaviour_module.ex
class Doctor.AnotherBehaviourModule (line 5) | defmodule Doctor.AnotherBehaviourModule
class Doctor.AnotherBehaviourModule.Behaviour (line 1) | defmodule Doctor.AnotherBehaviourModule.Behaviour
method func (line 9) | def func, do: "Hello world"
FILE: test/sample_files/behaviour_module.ex
class Doctor.BehaviourModule (line 1) | defmodule Doctor.BehaviourModule
method init (line 9) | def init(stack) do
method handle_call (line 15) | def handle_call(:pop, _from, [head | tail]) do
method handle_call (line 19) | def handle_call(:nop, _from, state) do
method handle_cast (line 24) | def handle_cast({:push, element}, state) do
FILE: test/sample_files/custom_behaviour_module.ex
class Doctor.FooBarBehaviour (line 1) | defmodule Doctor.FooBarBehaviour
class Doctor.FooBar (line 19) | defmodule Doctor.FooBar
method foo (line 26) | def foo(:five), do: 5
method foo (line 30) | def foo(:one), do: 1
method foo (line 33) | def foo(:two), do: 2
method bar (line 36) | def bar(:one), do: 1
method bar (line 37) | def bar(:two), do: 2
method bar (line 38) | def bar(:three), do: 3
method bar (line 41) | def bar(:test, value), do: value
method bar (line 45) | def bar(:bar, value), do: value
method bar (line 48) | def bar(:noop, _value1, _value2), do: 0
FILE: test/sample_files/derive_protocol.ex
class Doctor.DeriveProtocol (line 1) | defmodule Doctor.DeriveProtocol
FILE: test/sample_files/exception.ex
class Doctor.Exception (line 1) | defmodule Doctor.Exception
method exception (line 5) | def exception(value) do
FILE: test/sample_files/implement_protocol.ex
class Doctor.ImplementProtocol (line 1) | defmodule Doctor.ImplementProtocol
FILE: test/sample_files/nested_module.ex
class Doctor.ParentModule (line 1) | defmodule Doctor.ParentModule
method outer (line 22) | def outer, do: :ok
class Nested (line 6) | defmodule Nested
method inner (line 15) | def inner, do: :ok
FILE: test/sample_files/no_docs.ex
class Doctor.NoDocs (line 1) | defmodule Doctor.NoDocs
method func_1 (line 2) | def func_1(input) do
method func_2 (line 6) | def func_2(input), do: input + 2
method func_5 (line 14) | def func_5(input_1, input_2) do
method func_5 (line 18) | def func_5(input_1, input_2, input_3) do
method func_6 (line 22) | def func_6("match" = input), do: input
method func_6 (line 23) | def func_6("matches" = input), do: input
method func_6 (line 24) | def func_6("matcher" = input), do: input
method func_6 (line 25) | def func_6("matching" = input), do: input
method func_6 (line 26) | def func_6(_), do: "no match"
FILE: test/sample_files/no_struct_spec_module.ex
class Doctor.NoStructSpecModule (line 1) | defmodule Doctor.NoStructSpecModule
FILE: test/sample_files/opaque_struct_spec_module.ex
class Doctor.OpaqueStructSpecModule (line 1) | defmodule Doctor.OpaqueStructSpecModule
FILE: test/sample_files/partial_docs.ex
class Doctor.PartialDocs (line 1) | defmodule Doctor.PartialDocs
method func_1 (line 2) | def func_1(input) do
method func_2 (line 6) | def func_2(input), do: input + 2
method func_5 (line 18) | def func_5(input_1, input_2) do
method func_5 (line 24) | def func_5(input_1, input_2, input_3) do
method func_6 (line 30) | def func_6("match" = input), do: input
method func_6 (line 31) | def func_6("matches" = input), do: input
method func_6 (line 32) | def func_6("matcher" = input), do: input
method func_6 (line 33) | def func_6("matching" = input), do: input
method func_6 (line 34) | def func_6(_), do: "no match"
FILE: test/sample_files/struct_spec_module.ex
class Doctor.StructSpecModule (line 1) | defmodule Doctor.StructSpecModule
FILE: test/sample_files/use_module.ex
class Doctor.UseModule (line 1) | defmodule Doctor.UseModule
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (155K chars).
[
{
"path": ".doctor.exs",
"chars": 401,
"preview": "%Doctor.Config{\n ignore_modules: [],\n ignore_paths: [\n ~r(^test/sample_files/.*)\n ],\n min_module_doc_coverage: 80"
},
{
"path": ".formatter.exs",
"chars": 117,
"preview": "# Used by \"mix format\"\n[\n line_length: 120,\n inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 19,
"preview": "github: [akoutmos]\n"
},
{
"path": ".github/workflows/master.yml",
"chars": 1596,
"preview": "name: Doctor CI\n\nenv:\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\non:\n push:\n branches: [master]\n pull_request:\n "
},
{
"path": ".gitignore",
"chars": 576,
"preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
},
{
"path": "CHANGELOG.md",
"chars": 3099,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "LICENSE",
"chars": 1074,
"preview": "MIT License\n\nCopyright (c) 2018 Alexander Koutmos\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "README.md",
"chars": 24116,
"preview": "# Doctor\n\n[](https://hex.pm/packages/doc"
},
{
"path": "config/config.exs",
"chars": 1122,
"preview": "# This file is responsible for configuring your application\n# and its dependencies with the aid of the Mix.Config module"
},
{
"path": "coveralls.json",
"chars": 77,
"preview": "{\n \"minimum_coverage\": 75,\n \"skip_files\": [\n \"test/sample_files/\"\n ]\n}\n"
},
{
"path": "lib/cli/cli.ex",
"chars": 4471,
"preview": "defmodule Doctor.CLI do\n @moduledoc \"\"\"\n Provides the various CLI task entry points and CLI arg parsing.\n \"\"\"\n\n alia"
},
{
"path": "lib/config.ex",
"chars": 3850,
"preview": "defmodule Doctor.Config do\n @moduledoc \"\"\"\n This module defines a struct which houses all the\n configuration data for"
},
{
"path": "lib/docs.ex",
"chars": 586,
"preview": "defmodule Doctor.Docs do\n @moduledoc \"\"\"\n This module defines a struct which houses all the\n documentation data for m"
},
{
"path": "lib/doctor.ex",
"chars": 468,
"preview": "defmodule Doctor do\n @moduledoc \"\"\"\n Doctor is a utility which aims to provide insights into the health of your projec"
},
{
"path": "lib/mix/tasks/doctor.ex",
"chars": 5347,
"preview": "defmodule Mix.Tasks.Doctor do\n @moduledoc \"\"\"\n Doctor is a command line utility that can be used to ensure that your p"
},
{
"path": "lib/mix/tasks/doctor.explain.ex",
"chars": 5622,
"preview": "defmodule Mix.Tasks.Doctor.Explain do\n @moduledoc \"\"\"\n Figuring out why a particular module failed Doctor validation c"
},
{
"path": "lib/mix/tasks/doctor.gen.config.ex",
"chars": 1602,
"preview": "defmodule Mix.Tasks.Doctor.Gen.Config do\n @moduledoc \"\"\"\n Doctor is a command line utility that can be used to ensure "
},
{
"path": "lib/module_information.ex",
"chars": 10732,
"preview": "defmodule Doctor.ModuleInformation do\n @moduledoc \"\"\"\n This module defines a struct which houses all the\n documentati"
},
{
"path": "lib/module_report.ex",
"chars": 3850,
"preview": "defmodule Doctor.ModuleReport do\n @moduledoc \"\"\"\n This module exposes a struct which encapsulates all the results for "
},
{
"path": "lib/report_utils.ex",
"chars": 6355,
"preview": "defmodule Doctor.ReportUtils do\n @moduledoc \"\"\"\n This module provides some utility functions for use in report generat"
},
{
"path": "lib/reporter.ex",
"chars": 216,
"preview": "defmodule Doctor.Reporter do\n @moduledoc \"\"\"\n Defines the behaviour for a reporter\n \"\"\"\n\n @type module_reports :: [D"
},
{
"path": "lib/reporters/full.ex",
"chars": 4606,
"preview": "defmodule Doctor.Reporters.Full do\n @moduledoc \"\"\"\n This reporter generates a full documentation coverage report and l"
},
{
"path": "lib/reporters/module_explain.ex",
"chars": 7439,
"preview": "defmodule Doctor.Reporters.ModuleExplain do\n @moduledoc \"\"\"\n This module produces a report for a single project module"
},
{
"path": "lib/reporters/output_utils.ex",
"chars": 1906,
"preview": "defmodule Doctor.Reporters.OutputUtils do\n @moduledoc \"\"\"\n This module provides convenience functions for use when gen"
},
{
"path": "lib/reporters/short.ex",
"chars": 3972,
"preview": "defmodule Doctor.Reporters.Short do\n @moduledoc \"\"\"\n This reporter generates a full documentation coverage report and "
},
{
"path": "lib/reporters/summary.ex",
"chars": 1978,
"preview": "defmodule Doctor.Reporters.Summary do\n @moduledoc \"\"\"\n This reporter generates a short summary documentation coverage "
},
{
"path": "lib/specs.ex",
"chars": 471,
"preview": "defmodule Doctor.Specs do\n @moduledoc \"\"\"\n This module defines a struct which houses all the\n documentation data for "
},
{
"path": "mix.exs",
"chars": 1627,
"preview": "defmodule Doctor.MixProject do\n use Mix.Project\n\n @source_url \"https://github.com/akoutmos/doctor\"\n\n def project do\n "
},
{
"path": "test/config_test.exs",
"chars": 753,
"preview": "defmodule Doctor.ConfigTest do\n use ExUnit.Case, async: true\n alias Doctor.Config\n\n test \"config_defaults_as_string\" "
},
{
"path": "test/configs/exceptions_moduledoc_not_required.exs",
"chars": 369,
"preview": "%Doctor.Config{\n ignore_modules: [],\n ignore_paths: [],\n min_module_doc_coverage: 80,\n min_module_spec_coverage: 0,\n"
},
{
"path": "test/configs/exceptions_moduledoc_required.exs",
"chars": 368,
"preview": "%Doctor.Config{\n ignore_modules: [],\n ignore_paths: [],\n min_module_doc_coverage: 80,\n min_module_spec_coverage: 0,\n"
},
{
"path": "test/mix_doctor_test.exs",
"chars": 22414,
"preview": "defmodule Mix.Tasks.DoctorTest do\n use ExUnit.Case, async: false\n\n setup_all do\n original_shell = Mix.shell()\n M"
},
{
"path": "test/module_information_test.exs",
"chars": 2995,
"preview": "defmodule Doctor.ModuleInformationTest do\n use ExUnit.Case\n\n alias Doctor.{ModuleInformation, ModuleReport}\n\n describ"
},
{
"path": "test/module_report_test.exs",
"chars": 11385,
"preview": "defmodule Doctor.ModuleReportTest do\n use ExUnit.Case\n\n alias Doctor.{ModuleInformation, ModuleReport}\n\n test \"build/"
},
{
"path": "test/report_utils_test.exs",
"chars": 4744,
"preview": "defmodule Doctor.ReportUtilsTest do\n use ExUnit.Case\n import ExUnit.CaptureLog\n alias Doctor.{ModuleInformation, Modu"
},
{
"path": "test/sample_files/all_docs.ex",
"chars": 1143,
"preview": "defmodule Doctor.AllDocs do\n @moduledoc \"This is a module doc\"\n\n @spec func_1(integer()) :: integer()\n @doc \"Function"
},
{
"path": "test/sample_files/another_behaviour_module.ex",
"chars": 270,
"preview": "defmodule Doctor.AnotherBehaviourModule.Behaviour do\n @callback func() :: String.t()\nend\n\ndefmodule Doctor.AnotherBehav"
},
{
"path": "test/sample_files/behaviour_module.ex",
"chars": 490,
"preview": "defmodule Doctor.BehaviourModule do\n @moduledoc \"\"\"\n This is a GenServer module that has 100% code coverage\n \"\"\"\n\n u"
},
{
"path": "test/sample_files/custom_behaviour_module.ex",
"chars": 975,
"preview": "defmodule Doctor.FooBarBehaviour do\n @moduledoc \"\"\"\n A custom behaviour module\n \"\"\"\n\n @doc \"\"\"\n The famous foo func"
},
{
"path": "test/sample_files/derive_protocol.ex",
"chars": 208,
"preview": "defmodule Doctor.DeriveProtocol do\n @moduledoc \"\"\"\n An Example Derivation of a Protocol\n \"\"\"\n\n @derive Inspect\n def"
},
{
"path": "test/sample_files/exception.ex",
"chars": 190,
"preview": "defmodule Doctor.Exception do\n defexception [:message]\n\n @impl true\n def exception(value) do\n msg = \"doctor except"
},
{
"path": "test/sample_files/implement_protocol.ex",
"chars": 413,
"preview": "defmodule Doctor.ImplementProtocol do\n @moduledoc \"\"\"\n An Example Implementation of a Protocol\n \"\"\"\n\n defstruct [:fo"
},
{
"path": "test/sample_files/nested_module.ex",
"chars": 370,
"preview": "defmodule Doctor.ParentModule do\n @moduledoc \"\"\"\n A module containing another module\n \"\"\"\n\n defmodule Nested do\n "
},
{
"path": "test/sample_files/no_docs.ex",
"chars": 595,
"preview": "defmodule Doctor.NoDocs do\n def func_1(input) do\n input + 1\n end\n\n def func_2(input), do: input + 2\n\n def func_3("
},
{
"path": "test/sample_files/no_struct_spec_module.ex",
"chars": 71,
"preview": "defmodule Doctor.NoStructSpecModule do\n defstruct ~w(name arity)a\nend\n"
},
{
"path": "test/sample_files/opaque_struct_spec_module.ex",
"chars": 105,
"preview": "defmodule Doctor.OpaqueStructSpecModule do\n defstruct ~w(name arity)a\n\n @opaque t :: %__MODULE__{}\nend\n"
},
{
"path": "test/sample_files/partial_docs.ex",
"chars": 911,
"preview": "defmodule Doctor.PartialDocs do\n def func_1(input) do\n input + 1\n end\n\n def func_2(input), do: input + 2\n\n def fu"
},
{
"path": "test/sample_files/struct_spec_module.ex",
"chars": 157,
"preview": "defmodule Doctor.StructSpecModule do\n @type t :: %__MODULE__{\n name: atom(),\n arity: integer()\n "
},
{
"path": "test/sample_files/use_module.ex",
"chars": 468,
"preview": "defmodule Doctor.UseModule do\n @moduledoc \"\"\"\n A module with __using__ macro\n \"\"\"\n defmacro __using__(_opts) do\n "
},
{
"path": "test/test_helper.exs",
"chars": 15,
"preview": "ExUnit.start()\n"
}
]
About this extraction
This page contains the full source code of the akoutmos/doctor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (143.3 KB), approximately 35.6k tokens, and a symbol index with 245 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.