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