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
Current section
Files
lib/prop_wise/analyzer.ex
defmodule PropWise.Analyzer do
@moduledoc """
Main analyzer that combines purity analysis and pattern detection to score functions.
"""
alias PropWise.{Config, Parser, PatternDetector, PurityAnalyzer, SuggestionGenerator}
@doc """
Analyzes all functions in a project and returns candidates for property-based testing.
"""
def analyze_project(path, opts \\ []) do
min_score = Keyword.get(opts, :min_score, 4)
library = Keyword.get(opts, :library) || Config.library(path)
functions = Parser.parse_project(path)
all_scored_candidates =
functions
|> Enum.map(&analyze_function(&1, library))
candidates =
all_scored_candidates
|> Enum.filter(fn result -> result.score >= min_score end)
|> Enum.sort_by(& &1.score, :desc)
dropped_count =
all_scored_candidates
|> Enum.count(fn result -> result.score > 0 and result.score < min_score end)
inverse_pairs = PatternDetector.find_inverse_pairs(functions)
%{
candidates: candidates,
inverse_pairs: inverse_pairs,
total_functions: length(functions),
candidates_count: length(candidates),
dropped_count: dropped_count
}
end
@doc """
Analyzes a single function and returns a scored result.
"""
def analyze_function(function_info, library \\ :stream_data) do
purity = PurityAnalyzer.analyze(function_info)
patterns = PatternDetector.detect_patterns(function_info)
score = calculate_score(purity, patterns, function_info)
%{
module: function_info.module,
name: function_info.name,
arity: function_info.arity,
file: function_info.file,
line: function_info.line,
type: function_info.type,
purity: purity,
patterns: patterns,
score: score,
suggestions: generate_suggestions(patterns, function_info, library)
}
end
defp calculate_score({:impure, _}, _patterns, _function_info), do: 0
defp calculate_score({:pure, _}, patterns, function_info) do
base_score = 1
# Add points for detected patterns
pattern_score = length(patterns) * 2
# Bonus for having multiple patterns
multi_pattern_bonus = if length(patterns) >= 2, do: 2, else: 0
# Bonus for non-trivial functions (more than 3 lines or multiple clauses)
complexity_bonus = if complex_enough?(function_info), do: 1, else: 0
# Bonus for public functions
visibility_bonus = if function_info.type == :public, do: 1, else: 0
base_score + pattern_score + multi_pattern_bonus + complexity_bonus + visibility_bonus
end
defp complex_enough?(function_info) do
body_string = Macro.to_string(function_info.body)
line_count = body_string |> String.split("\n") |> length()
line_count > 3 or has_multiple_clauses?(function_info.body)
end
defp has_multiple_clauses?(body) do
match?({:case, _, _}, body) or match?({:cond, _, _}, body) or match?({:with, _, _}, body)
end
defp generate_suggestions(patterns, function_info, library) do
SuggestionGenerator.generate(patterns, function_info, library)
end
end