Packages

AST-based analyzer for identifying property-based testing candidates in Elixir codebases. Detects pure functions, identifies testable patterns, finds inverse function pairs, and generates concrete property-based test suggestions.

Current section

Files

Jump to
propwise mix.exs
Raw

mix.exs

defmodule Mix.Tasks.BundleDeps do
  use Mix.Task

  @shortdoc "Bundles compiled dependency beam and app files into propwise ebin"
  @moduledoc """
  Copies compiled dependency files (.beam and .app) into propwise's ebin directory
  so that when Mix builds an archive (.ez), all runtime dependencies are bundled.
  """
  def run(_args) do
    build_path = Mix.Project.build_path()
    lib_path = Path.join(build_path, "lib")
    app_name = Atom.to_string(Mix.Project.config()[:app])
    target_ebin = Path.join([lib_path, app_name, "ebin"])

    if File.dir?(lib_path) and File.dir?(target_ebin) do
      # Only bundle *runtime* dependencies: dev/test tooling (credo, dialyxir,
      # ex_doc, excoveralls) must never end up inside the published archive.
      runtime_deps = runtime_dep_names()

      lib_path
      |> File.ls!()
      |> Enum.reject(&(&1 == app_name))
      |> Enum.filter(&(&1 in runtime_deps))
      |> Enum.each(fn dep_app ->
        dep_ebin = Path.join([lib_path, dep_app, "ebin"])

        if File.dir?(dep_ebin) do
          dep_ebin
          |> Path.join("*")
          |> Path.wildcard()
          |> Enum.each(fn dep_file ->
            if File.regular?(dep_file) do
              dest = Path.join(target_ebin, Path.basename(dep_file))
              File.copy!(dep_file, dest)
            end
          end)
        end
      end)
    end

    {:ok, []}
  end

  defp runtime_dep_names do
    Mix.Project.config()
    |> Keyword.get(:deps, [])
    |> Enum.filter(fn dep ->
      opts = dep_opts(dep)
      Keyword.get(opts, :runtime, true) != false and Keyword.get(opts, :only) in [nil, :prod]
    end)
    |> Enum.map(fn dep -> dep |> elem(0) |> Atom.to_string() end)
  end

  defp dep_opts(dep) when is_tuple(dep),
    do: dep |> Tuple.to_list() |> List.last() |> normalize_opts()

  defp dep_opts(_), do: []

  defp normalize_opts(opts) when is_list(opts), do: opts
  defp normalize_opts(_), do: []
end

defmodule PropWise.MixProject do
  use Mix.Project

  @version "0.5.0"
  @source_url "https://github.com/Oeditus/propwise"

  def project do
    [
      app: :propwise,
      version: @version,
      elixir: "~> 1.18",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      escript: escript(),
      description: description(),
      package: package(),
      docs: docs(),
      aliases: aliases(),
      dialyzer: dialyzer(),
      test_coverage: [tool: ExCoveralls],
      name: "PropWise",
      source_url: @source_url,
      homepage_url: @source_url
    ]
  end

  def cli do
    [
      preferred_envs: [
        coveralls: :test,
        "coveralls.detail": :test,
        "coveralls.post": :test,
        "coveralls.html": :test,
        "coveralls.json": :test
      ]
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger, :jason, :metastatic]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:metastatic, "~> 0.29"},
      # Runtime dependency needed for JSON output
      {:jason, "~> 1.4"},
      {:ex_doc, "~> 0.34", only: :dev, runtime: false},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
      {:excoveralls, "~> 0.18", only: :test, runtime: false},
      # Dogfooding: PropWise recommends property-based tests, so it should have
      # some itself.
      {:stream_data, "~> 1.1", only: :test}
    ]
  end

  defp escript do
    [main_module: PropWise.CLI]
  end

  defp aliases do
    [
      "archive.build": ["compile", "bundle_deps", "archive.build --no-compile"],
      quality: ["format", "credo --strict", "dialyzer"],
      "quality.ci": [
        "format --check-formatted",
        "credo --strict",
        "dialyzer"
      ]
    ]
  end

  defp dialyzer do
    [
      plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
      plt_add_apps: [:mix]
    ]
  end

  defp description do
    """
    AST-based analyzer for identifying property-based testing candidates in Elixir codebases.
    Detects pure functions, identifies testable patterns, finds inverse function pairs,
    and generates concrete property-based test suggestions.
    """
  end

  defp package do
    [
      name: "propwise",
      files: ~w(lib mix.exs README.md LICENSE CHANGELOG.md stuff/docs/SCORING.md),
      licenses: ["MIT"],
      links: %{
        "GitHub" => @source_url,
        "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md"
      },
      maintainers: ["Aleksei Matiushkin"],
      # Enable installation as a Mix archive
      files_to_archive: ~w(lib mix.exs README.md LICENSE)
    ]
  end

  defp docs do
    [
      main: "readme",
      source_ref: "v#{@version}",
      source_url: @source_url,
      logo: "stuff/img/logo-48x48.png",
      extras: [
        "README.md",
        "CHANGELOG.md",
        "stuff/docs/SCORING.md",
        "LICENSE"
      ],
      groups_for_extras: [
        Guides: ~r/stuff\/docs\/.?/
      ],
      formatters: ["html"],
      authors: ["Aleksei Matiushkin"],
      api_reference: false,
      groups_for_modules: [
        Core: [
          PropWise,
          PropWise.Analyzer,
          PropWise.SuggestionGenerator
        ],
        "AST Analysis": [
          PropWise.Parser,
          PropWise.PurityAnalyzer,
          PropWise.PatternDetector
        ],
        Output: [
          PropWise.Reporter
        ],
        "Command Line": [
          PropWise.CommandLine,
          PropWise.CLI,
          Mix.Tasks.Propwise
        ],
        "Data Structures": [
          PropWise.FunctionInfo,
          PropWise.Candidate
        ],
        Configuration: [
          PropWise.Config
        ]
      ]
    ]
  end
end