Skip to content

Commit

Permalink
Compare formatted and unformatted ASTs during formatter tests
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Nov 12, 2023
1 parent 96b265c commit c029549
Show file tree
Hide file tree
Showing 3 changed files with 1,657 additions and 14 deletions.
41 changes: 41 additions & 0 deletions crates/ruff_python_ast/src/comparable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1480,3 +1480,44 @@ impl<'a> From<&'a ast::Stmt> for ComparableStmt<'a> {
}
}
}

#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableMod<'a> {
Module(ComparableModModule<'a>),
Expression(ComparableModExpression<'a>),
}

#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableModModule<'a> {
body: Vec<ComparableStmt<'a>>,
}

#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableModExpression<'a> {
body: Box<ComparableExpr<'a>>,
}

impl<'a> From<&'a ast::Mod> for ComparableMod<'a> {
fn from(mod_: &'a ast::Mod) -> Self {
match mod_ {
ast::Mod::Module(module) => Self::Module(module.into()),
ast::Mod::Expression(expr) => Self::Expression(expr.into()),
}
}
}

impl<'a> From<&'a ast::ModModule> for ComparableModModule<'a> {
fn from(module: &'a ast::ModModule) -> Self {
Self {
body: module.body.iter().map(Into::into).collect(),
}
}
}

impl<'a> From<&'a ast::ModExpression> for ComparableModExpression<'a> {
fn from(expr: &'a ast::ModExpression) -> Self {
Self {
body: (&expr.body).into(),
}
}
}
86 changes: 72 additions & 14 deletions crates/ruff_python_formatter/tests/fixtures.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use ruff_formatter::FormatOptions;
use ruff_python_formatter::{format_module_source, PreviewMode, PyFormatOptions};
use similar::TextDiff;
use std::fmt::{Formatter, Write};
use std::io::BufReader;
use std::path::Path;
use std::{fmt, fs};

use similar::TextDiff;

use normalized_ast::NormalizedMod;
use ruff_formatter::FormatOptions;
use ruff_python_formatter::{format_module_source, PreviewMode, PyFormatOptions};
use ruff_python_parser::{parse, AsMode};

mod normalized_ast;

#[test]
fn black_compatibility() {
let test_file = |input_path: &Path| {
Expand Down Expand Up @@ -33,7 +39,8 @@ fn black_compatibility() {

let formatted_code = printed.as_code();

ensure_stability_when_formatting_twice(formatted_code, options, input_path);
ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path);
ensure_unchanged_ast(&content, formatted_code, options.clone(), input_path);

if formatted_code == expected_output {
// Black and Ruff formatting matches. Delete any existing snapshot files because the Black output
Expand Down Expand Up @@ -112,6 +119,7 @@ fn format() {
let formatted_code = printed.as_code();

ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path);
ensure_unchanged_ast(&content, formatted_code, options.clone(), input_path);

let mut snapshot = format!("## Input\n{}", CodeFrame::new("python", &content));

Expand All @@ -129,6 +137,7 @@ fn format() {
let formatted_code = printed.as_code();

ensure_stability_when_formatting_twice(formatted_code, options.clone(), input_path);
ensure_unchanged_ast(&content, formatted_code, options.clone(), input_path);

writeln!(
snapshot,
Expand All @@ -140,12 +149,6 @@ fn format() {
.unwrap();
}
} else {
let printed =
format_module_source(&content, options.clone()).expect("Formatting to succeed");
let formatted = printed.as_code();

ensure_stability_when_formatting_twice(formatted, options.clone(), input_path);

// We want to capture the differences in the preview style in our fixtures
let options_preview = options.with_preview(PreviewMode::Enabled);
let printed_preview = format_module_source(&content, options_preview.clone())
Expand All @@ -157,12 +160,18 @@ fn format() {
options_preview.clone(),
input_path,
);
ensure_unchanged_ast(
&content,
formatted_preview,
options_preview.clone(),
input_path,
);

if formatted == formatted_preview {
if formatted_code == formatted_preview {
writeln!(
snapshot,
"## Output\n{}",
CodeFrame::new("python", &formatted)
CodeFrame::new("python", &formatted_code)
)
.unwrap();
} else {
Expand All @@ -171,10 +180,10 @@ fn format() {
writeln!(
snapshot,
"## Output\n{}\n## Preview changes\n{}",
CodeFrame::new("python", &formatted),
CodeFrame::new("python", &formatted_code),
CodeFrame::new(
"diff",
TextDiff::from_lines(formatted, formatted_preview)
TextDiff::from_lines(formatted_code, formatted_preview)
.unified_diff()
.header("Stable", "Preview")
)
Expand Down Expand Up @@ -239,6 +248,55 @@ Formatted twice:
}
}

/// Ensure that formatting doesn't change the AST.
///
/// Like Black, there are a few exceptions to this "invariant" which are encoded in
/// [`NormalizedMod`] and related structs. Namely, formatting can change indentation within strings,
/// and can also flatten tuples within `del` statements.
fn ensure_unchanged_ast(
unformatted_code: &str,
formatted_code: &str,
options: PyFormatOptions,
input_path: &Path,
) {
let source_type = options.source_type();

// Parse the unformatted code.
let unformatted_ast = parse(
unformatted_code,
source_type.as_mode(),
&input_path.to_string_lossy(),
)
.expect("Unformatted code to be valid syntax");
let unformatted_ast = NormalizedMod::from(&unformatted_ast);

// Parse the formatted code.
let formatted_ast = parse(
formatted_code,
source_type.as_mode(),
&input_path.to_string_lossy(),
)
.expect("Formatted code to be valid syntax");
let formatted_ast = NormalizedMod::from(&formatted_ast);

if formatted_ast != unformatted_ast {
let diff = TextDiff::from_lines(
&format!("{:#?}", unformatted_ast),
&format!("{:#?}", formatted_ast),
)
.unified_diff()
.header("Unformatted", "Formatted")
.to_string();
panic!(
r#"Reformatting the unformatted code of {} resulted in AST changes.
---
{diff}
"#,
input_path.display(),
);
}
}

struct Header<'a> {
title: &'a str,
}
Expand Down
Loading

0 comments on commit c029549

Please sign in to comment.