Packages
metastatic
0.13.2
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/metastatic.complexity.ex
defmodule Mix.Tasks.Metastatic.Complexity do
@moduledoc """
Analyzes code complexity for a given file.
## Usage
mix metastatic.complexity FILE [options]
## Options
* `--format` - Output format: text (default), json, or detailed
* `--language` - Source language: python, elixir, erlang, ruby, or haskell (auto-detected if not specified)
* `--max-cyclomatic` - Cyclomatic complexity threshold (default: 10)
* `--max-cognitive` - Cognitive complexity threshold (default: 15)
* `--max-nesting` - Nesting depth threshold (default: 3)
* `--lines` - Analyze specific line range (e.g., "10-50") - extracted code must be valid
* `--function` - Analyze specific function by name (all clauses combined for multi-clause functions)
## Examples
# Basic complexity analysis
mix metastatic.complexity my_file.py
# JSON output
mix metastatic.complexity my_file.py --format json
# Detailed report
mix metastatic.complexity my_file.ex --format detailed
# Custom thresholds
mix metastatic.complexity my_file.erl --max-cyclomatic 15 --max-cognitive 20
# Analyze specific line range
mix metastatic.complexity my_file.py --lines 10-50
# Analyze specific function (all clauses combined)
mix metastatic.complexity my_file.ex --function complex
mix metastatic.complexity my_file.ex --function factorial
## Exit Codes
* 0 - All metrics within thresholds
* 1 - Warnings (exceeded thresholds)
* 2 - Error during analysis
"""
@shortdoc "Analyzes code complexity"
use Mix.Task
@dialyzer {:no_return, run: 1}
@dialyzer {:no_return, analyze_file: 5}
alias Metastatic.Analysis.Complexity
alias Metastatic.Analysis.Complexity.Formatter
alias Metastatic.Builder
@impl Mix.Task
def run(args) do
Mix.Task.run("app.start")
{opts, files, _invalid} =
OptionParser.parse(args,
strict: [
format: :string,
language: :string,
max_cyclomatic: :integer,
max_cognitive: :integer,
max_nesting: :integer,
lines: :string,
function: :string
],
aliases: [f: :format, l: :language]
)
format = parse_format(opts[:format])
language = parse_language(opts[:language])
thresholds = build_thresholds(opts)
scope = parse_scope(opts)
case files do
[] ->
Mix.shell().error("Error: No file specified")
Mix.shell().info("Usage: mix metastatic.complexity FILE [options]")
exit({:shutdown, 2})
[file | _] ->
analyze_file(file, language, format, thresholds, scope)
end
end
defp analyze_file(file, language, format, thresholds, scope) do
unless File.exists?(file) do
Mix.shell().error("Error: File not found: #{file}")
exit({:shutdown, 2})
end
source = File.read!(file)
lang = language || detect_language(file)
# Extract subset of source if scope is specified
{source, scope_info} = extract_scope(source, scope, lang)
case Builder.from_source(source, lang) do
{:ok, document} ->
# If function scope, extract specific function for analysis
{analysis_doc, display_info} =
case {scope, scope_info} do
{{:function, func_name}, :extract_function} ->
extract_and_prepare_function(document, func_name)
_ ->
display_scope_info(document, scope, scope_info)
{document, nil}
end
case Complexity.analyze(analysis_doc, thresholds: thresholds) do
{:ok, result} ->
if display_info, do: Mix.shell().info(display_info <> "\n")
output = Formatter.format(result, format)
Mix.shell().info(output)
# Exit with appropriate code
exit_code = if Enum.empty?(result.warnings), do: 0, else: 1
exit({:shutdown, exit_code})
# {:error, reason} ->
# Mix.shell().error("Analysis error: #{inspect(reason)}")
# exit({:shutdown, 2})
end
{:error, reason} ->
Mix.shell().error("Parse error: #{inspect(reason)}")
exit({:shutdown, 2})
end
end
defp parse_format(nil), do: :text
defp parse_format("text"), do: :text
defp parse_format("json"), do: :json
defp parse_format("detailed"), do: :detailed
defp parse_format(other) do
Mix.shell().error("Unknown format: #{other}")
Mix.shell().info("Valid formats: text, json, detailed")
exit({:shutdown, 2})
end
defp parse_language(nil), do: nil
defp parse_language("python"), do: :python
defp parse_language("elixir"), do: :elixir
defp parse_language("erlang"), do: :erlang
defp parse_language("ruby"), do: :ruby
defp parse_language("haskell"), do: :haskell
defp parse_language(other) do
Mix.shell().error("Unknown language: #{other}")
Mix.shell().info("Valid languages: python, elixir, erlang, ruby, haskell")
exit({:shutdown, 2})
end
defp detect_language(file) do
case Path.extname(file) do
".py" ->
:python
".ex" ->
:elixir
".exs" ->
:elixir
".erl" ->
:erlang
".hrl" ->
:erlang
".rb" ->
:ruby
".hs" ->
:haskell
other ->
Mix.shell().error("Cannot detect language from extension: #{other}")
Mix.shell().info("Please specify --language option")
exit({:shutdown, 2})
end
end
defp build_thresholds(opts) do
thresholds = %{}
thresholds =
if max_cyc = opts[:max_cyclomatic] do
Map.put(thresholds, :cyclomatic_warning, max_cyc)
else
thresholds
end
thresholds =
if max_cog = opts[:max_cognitive] do
Map.put(thresholds, :cognitive_warning, max_cog)
else
thresholds
end
thresholds =
if max_nest = opts[:max_nesting] do
Map.put(thresholds, :nesting_warning, max_nest)
else
thresholds
end
thresholds
end
defp module_file?(%{ast: {:language_specific, meta, _}}) when is_list(meta) do
Keyword.get(meta, :hint) == :module_definition
end
defp module_file?(%{ast: {:container, _meta, _}}), do: true
defp module_file?(_), do: false
defp parse_scope(opts) do
cond do
opts[:function] -> {:function, opts[:function]}
opts[:lines] -> {:lines, parse_line_range(opts[:lines])}
true -> :full
end
end
defp parse_line_range(str) do
case String.split(str, "-") do
[start_str, end_str] ->
with {start_line, ""} <- Integer.parse(start_str),
{end_line, ""} <- Integer.parse(end_str) do
{start_line, end_line}
else
_ ->
Mix.shell().error("Invalid line range: #{str}")
Mix.shell().info("Expected format: START-END (e.g., 10-50)")
exit({:shutdown, 2})
end
_ ->
Mix.shell().error("Invalid line range: #{str}")
Mix.shell().info("Expected format: START-END (e.g., 10-50)")
exit({:shutdown, 2})
end
end
defp extract_scope(source, :full, _lang), do: {source, nil}
defp extract_scope(source, {:lines, {start_line, end_line}}, _lang) do
lines = String.split(source, "\n")
total_lines = length(lines)
cond do
start_line < 1 or end_line > total_lines ->
Mix.shell().error(
"Line range #{start_line}-#{end_line} out of bounds (file has #{total_lines} lines)"
)
exit({:shutdown, 2})
start_line > end_line ->
Mix.shell().error("Invalid line range: start (#{start_line}) > end (#{end_line})")
exit({:shutdown, 2})
true ->
extracted =
lines
|> Enum.slice((start_line - 1)..(end_line - 1))
|> Enum.join("\n")
{extracted, "lines #{start_line}-#{end_line}"}
end
end
defp extract_scope(source, {:function, _func_name}, _lang) do
# For function scope, return full source with special marker
# We'll handle extraction after parsing
{source, :extract_function}
end
defp extract_and_prepare_function(document, func_name) do
body = get_module_body(document.ast, document.metadata)
case find_function_in_body(body, func_name) do
{:ok, func_ast} ->
# Create a new document with just the function body
func_doc = Metastatic.Document.new(func_ast, document.language)
info = "Note: Analyzing function '#{func_name}'"
{func_doc, info}
:not_found ->
Mix.shell().error("Function '#{func_name}' not found in source")
exit({:shutdown, 2})
end
end
defp get_module_body({:language_specific, meta, _native_ast}, doc_metadata)
when is_list(meta) do
hint = Keyword.get(meta, :hint)
if hint == :module_definition do
body = Map.get(doc_metadata, :body)
case body do
{:block, _, _} -> body
nil -> {:block, [], []}
single -> {:block, [], [single]}
end
else
nil
end
end
defp get_module_body({:container, _meta, children}, _doc_metadata)
when is_list(children) do
{:block, [], children}
end
defp get_module_body(_ast, _metadata), do: nil
defp find_function_in_body(nil, _func_name), do: :not_found
defp find_function_in_body({:block, _meta, statements}, func_name)
when is_list(statements) do
# Find ALL clauses with the same name (3-tuple format)
matching_clauses =
Enum.filter(statements, fn
{:language_specific, meta, native_ast} when is_list(meta) ->
Keyword.get(meta, :hint) == :function_definition and
Map.get(native_ast, "function_name") == func_name
{:function_def, meta, _children} when is_list(meta) ->
Keyword.get(meta, :name) == func_name
_ ->
false
end)
case matching_clauses do
[] ->
:not_found
clauses ->
bodies =
Enum.map(clauses, fn
{:language_specific, _meta, native_ast} ->
Map.get(native_ast, "body")
{:function_def, _meta, children} ->
{:block, [], children}
end)
combined_ast =
case bodies do
[single] -> single
multiple -> {:block, [], multiple}
end
{:ok, combined_ast}
end
end
defp find_function_in_body(_body, _func_name), do: :not_found
defp display_scope_info(document, :full, nil) do
if module_file?(document) do
Mix.shell().info("Note: Analyzing entire module (includes all nested functions).\n")
end
:ok
end
defp display_scope_info(_document, _scope, scope_info) when is_binary(scope_info) do
Mix.shell().info("Note: Analyzing #{scope_info}.\n")
end
end