defmodule Mix.Tasks.Firebird.Target.Check do @moduledoc """ Check if Elixir source files are compilable to WASM. Analyzes source files for WASM compilability without compiling them. Reports unsupported features, warnings, and optimization suggestions. ## Usage # Check all @wasm annotated files mix firebird.target.check # Check specific files mix firebird.target.check lib/math.ex lib/utils.ex # Check specific directories mix firebird.target.check --dirs lib/wasm_modules # JSON output mix firebird.target.check --json ## Exit Codes - 0: All files are compilable - 1: One or more files have errors """ use Mix.Task @shortdoc "Check if Elixir files are compilable to WASM" @switches [ dirs: :string, json: :boolean, verbose: :boolean ] @aliases [ d: :dirs, v: :verbose ] @impl Mix.Task def run(args) do {opts, paths, _} = OptionParser.parse(args, switches: @switches, aliases: @aliases) json_mode = Keyword.get(opts, :json, false) verbose = Keyword.get(opts, :verbose, false) files = resolve_files(opts, paths) if Enum.empty?(files) do unless json_mode do Mix.shell().info("🔍 No files to check.") Mix.shell().info(" Add @wasm true annotations or specify files.") end :ok else {:ok, reports} = Firebird.Target.Diagnostics.analyze_files(files) if json_mode do output_json(reports) else output_human(reports, verbose) end compilable = Enum.count(reports, & &1.compilable) total = length(reports) if compilable < total do exit({:shutdown, 1}) end :ok end end defp resolve_files(opts, paths) do explicit = paths |> Enum.filter(&File.regular?/1) from_dirs = case Keyword.get(opts, :dirs) do nil -> if Enum.empty?(explicit) do # Default: scan lib/ for @wasm annotated files find_wasm_files(["lib"]) else [] end dirs_str -> dirs = String.split(dirs_str, ",", trim: true) find_wasm_files(dirs) end (explicit ++ from_dirs) |> Enum.sort() |> Enum.uniq() end defp find_wasm_files(dirs) do dirs |> Enum.flat_map(fn dir -> if File.dir?(dir) do Path.wildcard(Path.join(dir, "**/*.{ex,exs}")) else [] end end) |> Enum.filter(fn path -> case File.read(path) do {:ok, content} -> String.contains?(content, "@wasm true") or String.contains?(path, "wasm_modules") _ -> false end end) end defp output_json(reports) do data = %{ total: length(reports), compilable: Enum.count(reports, & &1.compilable), files: Enum.map(reports, fn r -> %{ file: r.file, module: if(r.module, do: to_string(r.module)), compilable: r.compilable, function_count: r.function_count, estimated_wasm_size: r.estimated_wasm_size, diagnostics: Enum.map(r.diagnostics, fn d -> %{severity: d.severity, message: d.message, line: d.line} end) } end) } Mix.shell().info(Jason.encode!(data, pretty: true)) end defp output_human(reports, verbose) do Mix.shell().info("🔍 Firebird WASM Compilability Check") Mix.shell().info("") Enum.each(reports, fn report -> status = if report.compilable, do: "✅", else: "❌" Mix.shell().info(" #{status} #{report.file}") if report.module do Mix.shell().info(" Module: #{report.module}") end Mix.shell().info(" Functions: #{report.function_count}") if report.estimated_wasm_size do Mix.shell().info(" Est. WASM size: ~#{report.estimated_wasm_size} bytes") end # Always show errors and warnings errors = Enum.filter(report.diagnostics, &(&1.severity == :error)) warnings = Enum.filter(report.diagnostics, &(&1.severity == :warning)) hints = Enum.filter(report.diagnostics, &(&1.severity == :hint)) Enum.each(errors, fn d -> loc = if d.line, do: " (line #{d.line})", else: "" Mix.shell().error(" ❌ #{d.message}#{loc}") end) Enum.each(warnings, fn d -> loc = if d.line, do: " (line #{d.line})", else: "" Mix.shell().info(" ⚠️ #{d.message}#{loc}") end) if verbose do Enum.each(hints, fn d -> Mix.shell().info(" 💡 #{d.message}") end) end Mix.shell().info("") end) compilable = Enum.count(reports, & &1.compilable) total = length(reports) total_funcs = Enum.map(reports, & &1.function_count) |> Enum.sum() Mix.shell().info( "📊 Summary: #{compilable}/#{total} files compilable, #{total_funcs} total functions" ) end end