Current section

Files

Jump to
mutare mix.exs
Raw

mix.exs

defmodule Mutare.MixProject do
  use Mix.Project

  @version "0.2.0"
  @source_url "https://github.com/foxbenjaminfox/mutare"

  # Internal modules deliberately kept `@moduledoc false`: the transform pipeline's
  # stages (documented as one unit on `Mutare.Transform`) and a few plumbing modules.
  # Public moduledocs still name them in prose — the references are worth keeping in
  # source — so rather than delete the links we tell ExDoc not to autolink to them
  # (they have no doc page), which also silences the "references X but it is hidden"
  # warnings. Matched as a prefix, so a member/type/submodule reference
  # (`Mutare.Mutator.Dispatch.mutations/3`, `Mutare.Transform.Uses.Harvest`) is covered
  # by its parent's entry. Add a module here when a new `@moduledoc false` module gets
  # referenced from a visible moduledoc (the warning tells you which). Also covers the
  # rare case of a *stdlib* module that's hidden the same way (`Module.ParallelChecker`,
  # named in `Mutare.Sandbox.CompilerOptions` — internal to the Elixir compiler, no doc
  # page of its own).
  @hidden_internal_modules ~w(
    Module.ParallelChecker
    Mutare.Transform.Resolve
    Mutare.Transform.Uses
    Mutare.Transform.Behaviours
    Mutare.Transform.Analyze
    Mutare.Transform.FunctionPlan
    Mutare.Transform.ModulePlan
    Mutare.Transform.Aliases
    Mutare.Transform.Candidate
    Mutare.Transform.SelectorEmit
    Mutare.Transform.LiftedEmit
    Mutare.Transform.CaseClauseEmit
    Mutare.Transform.FnClauseEmit
    Mutare.Transform.ReceiveClauseEmit
    Mutare.Transform.RescueEmit
    Mutare.Transform.ClauseVariants
    Mutare.Transform.ImportWitness
    Mutare.Transform.HostedEmit
    Mutare.Transform.GuardBuild
    Mutare.Transform.Tag
    Mutare.Transform.ClauseAST
    Mutare.Transform.BindingEscapeEmit
    Mutare.Transform.Config
    Mutare.Transform.ClauseGuardEmit
    Mutare.Transform.CoverageEmit
    Mutare.Runner.Hydrate
    Mutare.Runner.Compile
    Mutare.Runner.MutantRun
    Mutare.Sandbox.Seed
    Mutare.Sandbox.Mirror
    Mutare.Options.Registry
    Mutare.CallRouting.Registry
    Mutare.Mutator.Dispatch
    Mutare.Coverage.HelperTemplate
    Mutare.Ignore.Directive
    Mutare.Mutators.RegexLiteral.Tokens
  )

  # A couple of *typespecs* in visible modules reference a hidden internal type
  # (`Mutare.Schema.t`'s `ineffective_ignores` field uses `Mutare.Ignore.Directive.t`;
  # `Mutare.Calls`'s `module_key` re-exports `Mutare.Transform.Calls.module_key`;
  # `Mutare.Options.t`'s `skip_lifting` field uses `Mutare.Lifting.skip_entry`;
  # `Mutare.Sandbox.materialized`'s `seed` field uses `Mutare.Sandbox.Seed.summary`).
  # Typespec autolinking bypasses `:skip_code_autolink_to`, so those are silenced by the
  # *referencing* module instead — keep this list tight.
  @typespec_refs_to_hidden ~w(Mutare.Calls Mutare.Options Mutare.Sandbox Mutare.Schema)

  def project do
    [
      app: :mutare,
      version: @version,
      elixir: "~> 1.18",
      description: description(),
      package: package(),
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      dialyzer: dialyzer(),
      docs: docs(),
      aliases: aliases(),
      # PropCheck.App starts `PropCheck.CounterStrike`, which opens a single
      # on-disk DETS file (`_build/propcheck.ctex` by default) at *application
      # boot* — i.e. on every `mix test` invocation, whether or not a
      # `:property` test actually runs. Under `mix mutare --workers N` (N > 1),
      # several `mix test` OS processes boot concurrently against the *same*
      # sandboxed `_build`, and concurrent DETS opens/writes corrupt that file.
      # Once corrupted it stays corrupted on disk, so — with `--keep-sandbox`
      # — every subsequent run (any mutant, even a future invocation) fails
      # `PropCheck.CounterStrike.init/1` and the *entire* baseline suite reports
      # not-green, which mutare surfaces only as undifferentiated
      # `:harness_error` on every mutant. Found dogfooding mutare on itself
      # (`--workers 4`); see NOTES.md "PropCheck counter-examples DETS
      # corruption under concurrent workers". Give each `mix test` process
      # (identified by its own BEAM's OS pid) a private counter-examples file
      # while under mutation (`MUTARE_ACTIVE_MUTANT` is set on the baseline,
      # the coverage probe, and every mutant run — see `test/test_helper.exs`);
      # normal local/CI runs keep the shared, cross-run-cached default file.
      # Placed under the sandbox's own `_build` (every sandboxed `mix test` has
      # its cwd set there — `Sandbox.Command.Invocation`), not the OS-wide tmp
      # dir, so an ephemeral (non-`--keep-sandbox`) sandbox's teardown
      # (`File.rm_rf(sandbox)`) removes it too instead of leaking one file per
      # mutant run into `/tmp` forever.
      propcheck: [
        counter_examples:
          if System.get_env("MUTARE_ACTIVE_MUTANT") do
            Path.join(File.cwd!(), "_build/propcheck-#{System.pid()}.ctex")
          end
      ]
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp description do
    "A compile-once mutation testing tool for Elixir."
  end

  defp package do
    [
      maintainers: ["Benjamin Fox"],
      licenses: ["MIT"],
      links: %{
        "GitHub" => @source_url,
        "Changelog" => "https://hexdocs.pm/mutare/changelog.html"
      },
      # Hex's default file set already includes LICENSE*, README*, mix.exs,
      # and lib/; list it explicitly so the bundled demo projects don't slip
      # into the package while keeping the licence and docs in.
      files: ["lib", "mix.exs", "README.md", "CHANGELOG.md", "LICENSE"]
    ]
  end

  defp deps do
    [
      {:sourceror, "~> 1.12"},
      # Powers the `mix mutare.install` / `mix igniter.install mutare` generator
      # (see `Mix.Tasks.Mutare.Install`). Optional so it isn't forced on projects
      # that add Mutare by hand — the installer module is compiled away when absent
      # (`Code.ensure_loaded?(Igniter)` guard) — while still being fetched here so
      # the task compiles and is tested. A consumer who runs `mix igniter.install`
      # already has it.
      {:igniter, "~> 0.8", optional: true},
      {:propcheck, "~> 1.5", only: [:dev, :test]},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev], runtime: false},
      {:ex_doc, "~> 0.34", only: :dev, runtime: false}
    ]
  end

  defp docs do
    [
      main: "readme",
      source_url: @source_url,
      source_ref: "v#{@version}",
      extras: ["README.md", "guides/extending.md", "CHANGELOG.md", "LICENSE"],
      skip_code_autolink_to: &skip_autolink_to?/1,
      skip_undefined_reference_warnings_on: &(&1 in @typespec_refs_to_hidden),
      # Modules fall into the first group whose entry matches, so the explicit
      # lists win over the trailing catch-alls. "Internal" (`~r//`) sweeps up
      # everything else — the transform pipeline, sandbox, coverage plumbing, etc.
      # `@moduledoc false` modules never appear at all.
      groups_for_modules: [
        "Core API": [
          Mutare,
          Mix.Tasks.Mutare,
          Mutare.Ignore,
          Mutare.Run,
          Mutare.Runner,
          Mutare.Options,
          Mutare.Result,
          Mutare.Score,
          Mutare.Site
        ],
        "Writing mutators": [
          Mutare.Mutator,
          Mutare.Mutator.Structural,
          Mutare.Mutator.MacroHost,
          Mutare.Mutator.Mutation,
          Mutare.Mutator.Spec,
          Mutare.AST,
          Mutare.Test,
          Mutare.Test.RoutingExtension,
          Mutare.Calls,
          Mutare.Analyze
        ],
        "Writing extensions": [
          Mutare.Extension,
          Mutare.Extension.Spec,
          Mutare.CallRouting,
          Mutare.CallRouting.Registry,
          Mutare.CallRouting.Spec,
          Mutare.UseExpansion,
          Mutare.UseExpansion.Expansion,
          Mutare.UseExpansion.ContractError
        ],
        "Built-in mutators": ~r/^Mutare\.Mutators/,
        Reporters: [
          Mutare.Report,
          Mutare.Report.Json,
          Mutare.Report.Html,
          Mutare.Report.Sarif,
          Mutare.Report.Live,
          Mutare.Report.Live.Lines
        ],
        Internal: ~r//
      ]
    ]
  end

  # True when `ref` (a prose autolink target) names one of the deliberately-hidden
  # internal modules — the module itself, or any member/type/submodule under it.
  defp skip_autolink_to?(ref) do
    bare = ref |> String.replace_prefix("t:", "") |> String.replace_prefix("c:", "")

    Enum.any?(@hidden_internal_modules, fn mod ->
      bare == mod or String.starts_with?(bare, mod <> ".")
    end)
  end

  defp dialyzer do
    [
      # Keep the PLTs in a stable, cacheable location (e.g. for CI).
      plt_local_path: "priv/plts",
      plt_core_path: "priv/plts",
      plt_add_apps: [:mix, :ex_unit],
      # High-signal spec-accuracy checks on top of the defaults. `:unmatched_returns`
      # is intentionally left off: it fights idiomatic fire-and-forget side-effect
      # calls (File.rm/1, etc.) with no genuine bugs to show for it here.
      flags: [:error_handling, :extra_return, :missing_return]
    ]
  end

  defp aliases do
    [
      check: ["format --check-formatted", "credo", "dialyzer"]
    ]
  end
end