use crate::bin_cli::args::Commands;
use crate::bin_cli::dispatch::DispatchContext;
const FIXTURE_CARGO_TOML: &str = "[package]\nname = \"test-lib\"\nversion = \"0.1.0\"\nedition = \"2024\"\n";
const FIXTURE_SOURCE: &str = r#"
#[derive(Default)]
pub struct ResultData {
pub label: String,
}
pub fn maybe_result(flag: bool) -> Option<ResultData> {
if flag {
Some(ResultData { label: "found".to_string() })
} else {
None
}
}
#[derive(Default, Clone)]
pub struct Point {
pub x: i64,
pub y: i64,
pub label: Option<String>,
}
impl Point {
pub fn new(x: i64, y: i64, label: Option<String>) -> Self {
Point { x, y, label }
}
pub fn translate(&self, dx: i64, dy: i64, scale: Option<i64>) -> Point {
let factor = scale.unwrap_or(1);
Point {
x: self.x + dx * factor,
y: self.y + dy * factor,
label: self.label.clone(),
}
}
}
pub fn list_points(count: i64) -> Vec<Point> {
(0..count).map(|i| Point::new(i, i, None)).collect()
}
pub enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
pub fn describe_shape(shape: Shape) -> String {
match shape {
Shape::Circle { radius } => format!("circle r={radius}"),
Shape::Rectangle { width, height } => format!("rect {width}x{height}"),
}
}
#[derive(Default)]
pub struct Filter {
pub min_value: i64,
pub max_value: i64,
pub label: Option<String>,
}
pub fn apply_filter(data: Vec<i64>, filter: Filter) -> Vec<i64> {
data.into_iter()
.filter(|value| *value >= filter.min_value && *value <= filter.max_value)
.collect()
}
pub enum Status {
Active,
Inactive,
}
#[derive(Default)]
pub struct BatchInput {
#[serde(default)]
pub statuses: Vec<Status>,
}
pub fn count_active(input: BatchInput) -> i64 {
input.statuses.iter().filter(|status| matches!(status, Status::Active)).count() as i64
}
#[derive(Default)]
pub struct Address {
pub city: String,
}
#[derive(Default)]
pub struct Person {
pub name: String,
pub address: Address,
}
pub fn greet(person: Person) -> String {
format!("hi {} from {}", person.name, person.address.city)
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("{field}: {message}")]
InvalidField { field: String, message: String },
}
pub fn validate(value: i64) -> Result<i64, ValidationError> {
if value < 0 {
Err(ValidationError::InvalidField {
field: "value".to_string(),
message: "must be non-negative".to_string(),
})
} else {
Ok(value)
}
}
// The three constructs below target the three converter-generator defects found auditing
// two consumer repos against 0.67.6 (which removed `bad-argument-type`/`bad-return` from
// the scaffolded pyrefly suppressions on the claim that codegen now emits correct
// `_to_rust_*`/`_from_native_*` conversions for these boundaries -- this fixture proves that
// claim against the specific shapes it did not originally cover). ~keep
//
// - `ResponseTool.tool_type` carries `#[serde(rename = "type")]`, a Python reserved word. The
// `_to_rust_response_tool` converter and the `.pyi` `__init__` stub must agree on the emitted
// keyword-argument spelling (`type`, not `type_`) or pyrefly reports `[unexpected-keyword]`.
// - `Recipe.ingredients` is `Vec<Ingredient>` where `Ingredient` is itself a `has_default`
// struct, so `_to_rust_recipe` must convert each element with `_to_rust_ingredient`, not pass
// the raw `list[options.Ingredient]` straight through (pyrefly `[bad-argument-type]`).
// - `Task` has two independent optional simple-enum fields (`priority`, `mode`) on one
// constructor call. Both are `Option<Enum>` in the native binding, so the emitted converter
// used to route them through a `**({...} if ... else {})` omission trick that isn't needed for
// an already-optional field -- and two such unpacks in one call is exactly the shape that made
// pyrefly cross-assign the two enum types between the two parameters.
#[derive(Default)]
pub struct ResponseTool {
#[serde(rename = "type")]
pub tool_type: String,
pub label: Option<String>,
}
pub fn describe_tool(tool: ResponseTool) -> String {
format!("{}: {}", tool.tool_type, tool.label.unwrap_or_default())
}
#[derive(Default, Clone)]
pub struct Ingredient {
pub name: String,
}
#[derive(Default)]
pub struct Recipe {
pub title: String,
pub ingredients: Vec<Ingredient>,
}
pub fn total_ingredients(recipe: Recipe) -> i64 {
recipe.ingredients.len() as i64
}
pub enum Priority {
Low,
High,
}
pub enum Mode {
Fast,
Slow,
}
#[derive(Default)]
pub struct Task {
pub title: String,
pub priority: Option<Priority>,
pub mode: Option<Mode>,
}
pub fn describe_task(task: Task) -> String {
let priority = match task.priority {
Some(Priority::Low) => "low",
Some(Priority::High) => "high",
None => "unset",
};
let mode = match task.mode {
Some(Mode::Fast) => "fast",
Some(Mode::Slow) => "slow",
None => "unset",
};
format!("{}: {priority}/{mode}", task.title)
}
// Three NON-`Option` enum fields, each carrying bare `#[serde(default)]`, on one config struct.
// `Task` above covers the `Option<Enum>` form; this covers the form that still routed through the
// `**({...} if x is not None else {})` omission trick. `options.py` renders each of these as the
// enum's `#[default]` variant string and never as `None`, so the guard is statically always true
// -- and pyrefly resolves an unpacked keyword against every remaining parameter, so N unpacks in
// one constructor call cost N*(N-1) `[bad-argument-type]` errors. Three fields is the smallest
// count that makes the cost visible as a cluster rather than a single pair. ~keep
#[derive(Default, Clone, Copy)]
pub enum Alignment {
#[default]
Start,
End,
}
#[derive(Default, Clone, Copy)]
pub enum Density {
#[default]
Loose,
Tight,
}
#[derive(Default, Clone, Copy)]
pub enum Casing {
#[default]
Lower,
Upper,
}
#[derive(Default)]
pub struct LayoutSpec {
pub title: String,
#[serde(default)]
pub alignment: Alignment,
#[serde(default)]
pub density: Density,
#[serde(default)]
pub casing: Casing,
}
pub fn describe_layout(spec: LayoutSpec) -> String {
let alignment = match spec.alignment {
Alignment::Start => "start",
Alignment::End => "end",
};
let density = match spec.density {
Density::Loose => "loose",
Density::Tight => "tight",
};
let casing = match spec.casing {
Casing::Lower => "lower",
Casing::Upper => "upper",
};
format!("{}: {alignment}/{density}/{casing}", spec.title)
}
"#;
const FIXTURE_ALEF_TOML: &str = r#"
[workspace]
languages = ["python"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
version_from = "Cargo.toml"
[crates.python]
module_name = "test_lib"
[crates.python.stubs]
output = "packages/python/test_lib"
"#;
fn write_fixture_workspace(root: &std::path::Path) {
std::fs::create_dir_all(root.join("src")).expect("create fixture src directory");
std::fs::write(root.join("src/lib.rs"), FIXTURE_SOURCE).expect("write fixture source");
std::fs::write(root.join("Cargo.toml"), FIXTURE_CARGO_TOML).expect("write fixture Cargo.toml");
std::fs::write(root.join("alef.toml"), FIXTURE_ALEF_TOML).expect("write fixture alef.toml");
}
fn find_pyrefly_project_dir(root: &std::path::Path) -> std::path::PathBuf {
for entry in walkdir_pyproject_tomls(root) {
let content = std::fs::read_to_string(&entry).unwrap_or_default();
if content.contains("[tool.pyrefly]") {
return entry
.parent()
.expect("pyproject.toml has a parent directory")
.to_path_buf();
}
}
panic!("no scaffolded pyproject.toml with a [tool.pyrefly] section found under {root:?}");
}
fn walkdir_pyproject_tomls(root: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut found = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.file_name().is_some_and(|name| name == "pyproject.toml") {
found.push(path);
}
}
}
found
}
#[test]
fn alef_all_generated_python_package_type_checks_clean_under_pyrefly() {
if which::which("pyrefly").is_err() {
return;
}
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path().canonicalize().unwrap_or_else(|_| dir.path().to_path_buf());
write_fixture_workspace(&root);
let _cwd = crate::test_support::CwdGuard::enter(&root);
let context = DispatchContext {
config_path: root.join("alef.toml"),
crate_filter: Vec::new(),
};
super::handle(
Commands::All {
clean: false,
clobber_create_once_seeds: false,
strict: false,
skip_frb: false,
skip_snippet_validation: true,
skip_compile: false,
},
&context,
)
.expect("alef all must succeed against a plain python fixture");
let api_py = root.join("packages/python/test_lib/api.py");
assert!(
api_py.is_file(),
"sanity: alef all must have written api.py, got tree under {root:?}"
);
let project_dir = find_pyrefly_project_dir(&root);
let output = std::process::Command::new("pyrefly")
.arg("check")
.arg(&project_dir)
.output()
.expect("pyrefly check must run");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"pyrefly must report zero errors against alef's own generated package \
(a `bad-return`/`bad-argument-type` here means the public-dataclass boundary fix \
regressed); pyrefly stdout:\n{stdout}\npyrefly stderr:\n{stderr}"
);
}