# PropWise
An AST-based analyzer for identifying property-based testing candidates in Elixir codebases.
## Overview
PropWise analyzes your Elixir code to find functions that would benefit from property-based testing. It examines the Abstract Syntax Tree (AST) of your code to:
- Detect pure functions (functions without side effects)
- Identify common patterns suitable for property testing
- Find inverse function pairs (encode/decode, serialize/deserialize, etc.)
- Score and rank candidates by testability
- Provide specific testing suggestions for each candidate
## Features
### Purity Analysis
Detects side effects by analyzing function calls:
- I/O operations (File, IO)
- Process operations (GenServer, Agent, Task)
- Database operations (Ecto)
- HTTP requests
- System calls
- Message passing
### Pattern Detection
Identifies functions with characteristics ideal for property testing:
- **Collection Operations**: Functions using Enum, Stream, or list comprehensions
- **Data Transformations**: Pipeline operations, struct/map manipulation
- **Validation Functions**: Boolean predicates and validation logic
- **Algebraic Structures**: Merge, concat, union, and other potentially algebraic operations
- **Encoders/Decoders**: Serialization and parsing functions
- **Numeric Algorithms**: Arithmetic and mathematical operations
### Inverse Pair Detection
Finds function pairs that are inverses of each other:
- encode/decode
- serialize/deserialize
- parse/format or parse/generate
- compress/decompress
- encrypt/decrypt
- to_*/from_*
- pack/unpack
- marshal/unmarshal
### Concrete Test Generation
Generates ready-to-use property-based test code:
- Supports multiple libraries: `stream_data` (default) and `PropEr`
- Specific test properties tailored to detected patterns
- Complete test blocks with appropriate generators
- Assertions matching the function's expected behavior
- Copy-paste ready test code to get started quickly
## Installation
### As a Library
Add `propwise` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:propwise, "~> 0.1.0"}
]
end
```
### As a Command-Line Tool
#### Option 1: escript (Recommended for standalone use)
Build and install the standalone executable:
```bash
cd propwise
mix deps.get
mix escript.build
# Copy to a directory in your PATH
sudo cp propwise /usr/local/bin/
# Or just use it directly
./propwise
```
The escript bundles all dependencies and works without Mix or any additional setup.
#### Option 2: Mix archive
Install globally from Hex as a Mix archive:
```bash
mix archive.install hex propwise
```
This makes the `mix propwise` task available in any project. Note: This requires `jason` to be available in your Mix environment.
To uninstall:
```bash
mix archive.uninstall propwise
```
#### Option 3: As a dependency
When added as a project dependency, PropWise provides a Mix task:
```bash
mix propwise
```
## Usage
### Command Line
#### Using escript
```bash
# Analyze current project
./propwise .
# Analyze with custom minimum score
./propwise --min-score 5 ./my_project
# Output as JSON
./propwise --format json ./my_project
# Use PropEr instead of stream_data
./propwise --library proper ./my_project
# Show help
./propwise --help
```
#### Using Mix task
```bash
# Analyze current project
mix propwise
# Analyze with custom minimum score
mix propwise --min-score 5
# Output as JSON
mix propwise --format json
# Use PropEr instead of stream_data
mix propwise --library proper
# Analyze another project
mix propwise ../other_project
# Show help
mix propwise --help
```
### As a Library
```elixir
# Analyze a project
result = PropWise.analyze("./my_project")
# Analyze with custom options
result = PropWise.analyze("./my_project", min_score: 5, library: :proper)
# Print the report
PropWise.print_report(result)
# Print as JSON
PropWise.print_report(result, format: :json)
```
## Example Output
```
================================================================================
PropWise Analysis Report
================================================================================
Summary:
Total functions analyzed: 143
Property test candidates: 24
Coverage: 16.8%
--------------------------------------------------------------------------------
Inverse Function Pairs Detected:
--------------------------------------------------------------------------------
MyApp.Encoder.encode/1 <-> decode/1
Suggestion: Test round-trip property: decode(encode(x)) == x
--------------------------------------------------------------------------------
Top Candidates (sorted by score):
--------------------------------------------------------------------------------
MyApp.Parser.parse_json/1
Score: 8
Location: lib/my_app/parser.ex:42
Type: public
Patterns:
- Parser: Parser function
- Data Transformation: Pipeline transformation
Testing suggestions:
- property "parse returns expected structure" do
check all input <- string(:alphanumeric) do
case Parser.parse_json(input) do
{:ok, result} -> assert valid_parsed_structure?(result)
{:error, _} -> true
end
end
end
- property "parse/format round-trip" do
check all data <- valid_data_generator() do
formatted = Parser.format(data)
assert Parser.parse_json(formatted) == {:ok, data}
end
end
- property "maintains structural invariants" do
check all input <- term() do
result = Parser.parse_json(input)
# Add your invariant checks here
assert valid_structure?(result)
end
end
MyApp.List.merge_sorted/2
Score: 7
Location: lib/my_app/list.ex:15
Type: public
Patterns:
- Collection Operation: Uses Enum collection operations
- Algebraic Structure: Potentially algebraic operation
Testing suggestions:
- property "preserves input size" do
check all list <- list_of(term()) do
assert length(List.merge_sorted(list)) == length(list)
end
end
- property "associativity" do
check all a <- term(), b <- term(), c <- term() do
assert List.merge_sorted(List.merge_sorted(a, b), c) ==
List.merge_sorted(a, List.merge_sorted(b, c))
end
end
```
## Scoring System
Functions are scored based on multiple factors:
- **Base score**: 1 point for pure functions
- **Pattern detection**: 2 points per detected pattern
- **Multiple patterns**: 2 bonus points for functions with 2+ patterns
- **Complexity**: 1 bonus point for non-trivial functions
- **Visibility**: 1 bonus point for public functions
Default minimum score is 3, but this can be adjusted based on your needs.
**For detailed information about all detection criteria and scoring rules, see [Scoring](stuff/docs/SCORING.md).**
## Configuration
You can customize PropWise's behavior by creating a `.propwise.exs` file in your project root.
### Example Configuration
```elixir
# .propwise.exs
%{
# Directories to analyze (relative to project root)
# Default: ["lib"]
analyze_paths: ["lib"],
# Property-based testing library to use for suggestions
# Options: :stream_data (default) or :proper
library: :stream_data
# You can analyze multiple directories:
# analyze_paths: ["lib", "src", "apps/my_app/lib"]
}
```
### Configuration Options
- `analyze_paths` - List of directories to analyze relative to project root (default: `["lib"]`)
- `library` - Property testing library for code generation: `:stream_data` or `:proper` (default: `:stream_data`)
If no `.propwise.exs` file is present, PropWise will use the defaults.
## Options
### CLI Options
- `-m, --min-score NUM`: Minimum score for candidates (default: 3)
- `-f, --format FORMAT`: Output format: text or json (default: text)
- `-l, --library LIB`: Property testing library: stream_data or proper (default: stream_data)
- `-h, --help`: Show help message
Note: CLI options override configuration file settings.
### Library Options
- `:min_score` - Minimum score threshold (integer, default: 3)
- `:format` - Output format (`:text` or `:json`, default: `:text`)
- `:library` - Property testing library (`:stream_data` or `:proper`, default: `:stream_data`)
## How It Works
1. **Parse**: Recursively finds all `.ex` files in configured directories (default: `lib`)
2. **Extract**: Parses each file's AST and extracts function definitions
3. **Analyze Purity**: Walks the AST to detect side effects
4. **Detect Patterns**: Looks for common patterns in function structure and naming
5. **Score**: Calculates a testability score for each function
6. **Find Pairs**: Identifies inverse function pairs across the codebase
7. **Generate Suggestions**: Creates concrete property-based test examples using your chosen library
8. **Report**: Presents findings with ready-to-use test code
## Limitations
- Static analysis only - doesn't execute code
- May produce false positives for functions that call other module functions (can't determine if those are pure)
- Pattern detection is heuristic-based
- Doesn't analyze macros or dynamically generated code in depth
## Contributing
Contributions are welcome! Areas for improvement:
- Additional pattern detectors
- Smarter purity analysis (tracking function calls across modules)
- Integration with existing property testing libraries
- Configuration file support
- IDE integration
## License
MIT