Full Code of akoutmos/doctor for AI

master 0e3750ca205b cached
50 files
143.3 KB
35.6k tokens
245 symbols
1 requests
Download .txt
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

[![Module Version](https://img.shields.io/hexpm/v/doctor.svg?style=for-the-badge)](https://hex.pm/packages/doctor)
[![Doctor CI](https://img.shields.io/github/actions/workflow/status/akoutmos/doctor/master.yml?label=Build%20Status&style=for-the-badge&branch=master)](https://github.com/akoutmos/doctor/actions)
[![Coverage Status](https://img.shields.io/coverallsCoverage/github/akoutmos/doctor.svg?branch=master&style=for-the-badge)](https://coveralls.io/github/akoutmos/doctor?branch=master)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg?style=for-the-badge)](https://hexdocs.pm/doctor/)
[![Total Download](https://img.shields.io/hexpm/dt/doctor.svg?style=for-the-badge)](https://hex.pm/packages/doctor)
[![License](https://img.shields.io/hexpm/l/doctor.svg?style=for-the-badge)](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()
Download .txt
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
Download .txt
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[![Module Version](https://img.shields.io/hexpm/v/doctor.svg?style=for-the-badge)](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.

Copied to clipboard!