diff --git a/Cargo.lock b/Cargo.lock index 76b5625539d7e0..1b4862769d710c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2966,6 +2966,7 @@ dependencies = [ "ruff_graph", "ruff_linter", "ruff_macros", + "ruff_markdown", "ruff_notebook", "ruff_options_metadata", "ruff_python_ast", @@ -3265,6 +3266,18 @@ dependencies = [ "syn", ] +[[package]] +name = "ruff_markdown" +version = "0.0.0" +dependencies = [ + "insta", + "regex", + "ruff_python_ast", + "ruff_python_formatter", + "ruff_python_trivia", + "ruff_workspace", +] + [[package]] name = "ruff_memory_usage" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 85654b3c492545..b60334e3853d4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ ruff_graph = { path = "crates/ruff_graph" } ruff_index = { path = "crates/ruff_index" } ruff_linter = { path = "crates/ruff_linter" } ruff_macros = { path = "crates/ruff_macros" } +ruff_markdown = { path = "crates/ruff_markdown" } ruff_memory_usage = { path = "crates/ruff_memory_usage" } ruff_notebook = { path = "crates/ruff_notebook" } ruff_options_metadata = { path = "crates/ruff_options_metadata" } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 0131fef2d357e3..569c3629b01f91 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -26,6 +26,7 @@ ruff_diagnostics = { workspace = true } ruff_graph = { workspace = true, features = ["serde", "clap"] } ruff_linter = { workspace = true, features = ["clap"] } ruff_macros = { workspace = true } +ruff_markdown = { workspace = true } ruff_notebook = { workspace = true } ruff_options_metadata = { workspace = true, features = ["serde"] } ruff_python_ast = { workspace = true } diff --git a/crates/ruff/resources/test/fixtures/unformatted.md b/crates/ruff/resources/test/fixtures/unformatted.md new file mode 100644 index 00000000000000..b8cd6913e47fba --- /dev/null +++ b/crates/ruff/resources/test/fixtures/unformatted.md @@ -0,0 +1,11 @@ +This is a markdown document with two fenced code blocks: + +```py +print( "hello" ) +def foo(): pass +``` + +```pyi +print( "hello" ) +def foo(): pass +``` diff --git a/crates/ruff/src/cache.rs b/crates/ruff/src/cache.rs index 6b696b55d3660c..81cfe99f3dbae8 100644 --- a/crates/ruff/src/cache.rs +++ b/crates/ruff/src/cache.rs @@ -486,6 +486,7 @@ mod tests { use anyhow::Result; use filetime::{FileTime, set_file_mtime}; use itertools::Itertools; + use ruff_python_ast::SourceType; use test_case::test_case; use ruff_cache::CACHE_DIR_NAME; @@ -1024,7 +1025,7 @@ mod tests { format_path( &file_path, &self.settings.formatter, - PySourceType::Python, + SourceType::Python(PySourceType::Python), FormatMode::Write, None, Some(cache), diff --git a/crates/ruff/src/commands/add_noqa.rs b/crates/ruff/src/commands/add_noqa.rs index ff6a07c758a8ef..928c5ce03e5b3a 100644 --- a/crates/ruff/src/commands/add_noqa.rs +++ b/crates/ruff/src/commands/add_noqa.rs @@ -69,7 +69,7 @@ pub(crate) fn add_noqa( { return None; } - let source_kind = match SourceKind::from_path(path, source_type) { + let source_kind = match SourceKind::from_path(path, SourceType::Python(source_type)) { Ok(Some(source_kind)) => source_kind, Ok(None) => return None, Err(e) => { diff --git a/crates/ruff/src/commands/analyze_graph.rs b/crates/ruff/src/commands/analyze_graph.rs index ba9ccf8c6ed7f8..48a2ec0f7d62a0 100644 --- a/crates/ruff/src/commands/analyze_graph.rs +++ b/crates/ruff/src/commands/analyze_graph.rs @@ -144,6 +144,10 @@ pub(crate) fn analyze_graph( debug!("Ignoring TOML file: {}", path.display()); continue; } + SourceType::Markdown => { + debug!("Ignoring Markdown file: {}", path.display()); + continue; + } }, Some(language) => PySourceType::from(language), }; @@ -164,7 +168,10 @@ pub(crate) fn analyze_graph( let result = inner_result.clone(); scope.spawn(move |_| { // Extract source code (handles both .py and .ipynb files) - let source_kind = match SourceKind::from_path(path.as_std_path(), source_type) { + let source_kind = match SourceKind::from_path( + path.as_std_path(), + SourceType::Python(source_type), + ) { Ok(Some(source_kind)) => source_kind, Ok(None) => { debug!("Skipping non-Python notebook: {path}"); diff --git a/crates/ruff/src/commands/format.rs b/crates/ruff/src/commands/format.rs index eb768bee58e187..b6619442ecd9aa 100644 --- a/crates/ruff/src/commands/format.rs +++ b/crates/ruff/src/commands/format.rs @@ -16,6 +16,7 @@ use ruff_db::diagnostic::{ }; use ruff_linter::message::{EmitterContext, create_panic_diagnostic, render_diagnostics}; use ruff_linter::settings::types::OutputFormat; +use ruff_markdown::{MarkdownResult, format_code_blocks}; use ruff_notebook::NotebookIndex; use ruff_python_parser::ParseError; use rustc_hash::{FxHashMap, FxHashSet}; @@ -123,15 +124,13 @@ pub(crate) fn format( let settings = resolver.resolve(path); let source_type = match settings.formatter.extension.get(path) { - None => match SourceType::from(path) { - SourceType::Python(source_type) => source_type, - SourceType::Toml(_) => { - // Ignore any non-Python files. - return None; - } - }, - Some(language) => PySourceType::from(language), + None => SourceType::from(path), + Some(language) => SourceType::Python(PySourceType::from(language)), }; + if source_type.is_toml() { + // Ignore TOML files. + return None; + } // Ignore files that are excluded from formatting if (settings.file_resolver.force_exclude || !resolved_file.is_root()) @@ -261,7 +260,7 @@ pub(crate) fn format( pub(crate) fn format_path( path: &Path, settings: &FormatterSettings, - source_type: PySourceType, + source_type: SourceType, mode: FormatMode, range: Option, cache: Option<&Cache>, @@ -292,8 +291,7 @@ pub(crate) fn format_path( let cache = cache.filter(|_| range.is_none()); // Format the source. - let format_result = match format_source(&unformatted, source_type, Some(path), settings, range)? - { + let format_result = match format_source(&unformatted, Some(path), settings, range)? { FormattedSource::Formatted(formatted) => match mode { FormatMode::Write => { let mut writer = File::create(path).map_err(|err| { @@ -357,14 +355,17 @@ impl From for FormatResult { /// unchanged. pub(crate) fn format_source( source_kind: &SourceKind, - source_type: PySourceType, path: Option<&Path>, settings: &FormatterSettings, range: Option, ) -> Result { match &source_kind { - SourceKind::Python(unformatted) => { - let options = settings.to_format_options(source_type, unformatted, path); + SourceKind::Python { + code: unformatted, + is_stub, + } => { + let py_source_type = source_kind.py_source_type(); + let options = settings.to_format_options(py_source_type, unformatted, path); let formatted = if let Some(range) = range { let line_index = LineIndex::from_source_text(unformatted); @@ -400,7 +401,10 @@ pub(crate) fn format_source( if formatted.len() == unformatted.len() && formatted == *unformatted { Ok(FormattedSource::Unchanged) } else { - Ok(FormattedSource::Formatted(SourceKind::Python(formatted))) + Ok(FormattedSource::Formatted(SourceKind::Python { + code: formatted, + is_stub: *is_stub, + })) } } SourceKind::IpyNotebook(notebook) => { @@ -409,12 +413,13 @@ pub(crate) fn format_source( } if range.is_some() { - return Err(FormatCommandError::RangeFormatNotebook( + return Err(FormatCommandError::RangeFormatNotSupported( path.map(Path::to_path_buf), )); } - let options = settings.to_format_options(source_type, notebook.source_code(), path); + let options = + settings.to_format_options(PySourceType::Ipynb, notebook.source_code(), path); let mut output: Option = None; let mut last: Option = None; @@ -489,6 +494,26 @@ pub(crate) fn format_source( formatted, ))) } + SourceKind::Markdown(unformatted_document) => { + if !settings.preview.is_enabled() { + return Err(FormatCommandError::MarkdownExperimental( + path.map(Path::to_path_buf), + )); + } + + if range.is_some() { + return Err(FormatCommandError::RangeFormatNotSupported( + path.map(Path::to_path_buf), + )); + } + + match format_code_blocks(unformatted_document, path, settings) { + MarkdownResult::Formatted(formatted) => { + Ok(FormattedSource::Formatted(SourceKind::Markdown(formatted))) + } + MarkdownResult::Unchanged => Ok(FormattedSource::Unchanged), + } + } } } @@ -830,7 +855,8 @@ pub(crate) enum FormatCommandError { Read(Option, SourceError), Format(Option, FormatModuleError), Write(Option, SourceError), - RangeFormatNotebook(Option), + RangeFormatNotSupported(Option), + MarkdownExperimental(Option), } impl FormatCommandError { @@ -848,7 +874,8 @@ impl FormatCommandError { | Self::Read(path, _) | Self::Format(path, _) | Self::Write(path, _) - | Self::RangeFormatNotebook(path) => path.as_deref(), + | Self::RangeFormatNotSupported(path) + | Self::MarkdownExperimental(path) => path.as_deref(), } } } @@ -880,10 +907,15 @@ impl From<&FormatCommandError> for Diagnostic { Diagnostic::new(DiagnosticId::Io, Severity::Error, source_error) } FormatCommandError::Format(_, format_module_error) => format_module_error.into(), - FormatCommandError::RangeFormatNotebook(_) => Diagnostic::new( + FormatCommandError::RangeFormatNotSupported(_) => Diagnostic::new( DiagnosticId::InvalidCliOption, Severity::Error, - "Range formatting isn't supported for notebooks.", + "Range formatting is only supported for Python files.", + ), + FormatCommandError::MarkdownExperimental(_) => Diagnostic::new( + DiagnosticId::PreviewFeature, + Severity::Warning, + "Markdown formatting is experimental, enable preview mode.", ), }; @@ -962,11 +994,11 @@ impl Display for FormatCommandError { write!(f, "{header} {err}", header = "Failed to format:".bold()) } } - Self::RangeFormatNotebook(path) => { + Self::RangeFormatNotSupported(path) => { if let Some(path) = path { write!( f, - "{header}{path}{colon} Range formatting isn't supported for notebooks.", + "{header}{path}{colon} Range formatting is only supported for Python files.", header = "Failed to format ".bold(), path = fs::relativize_path(path).bold(), colon = ":".bold() @@ -974,7 +1006,24 @@ impl Display for FormatCommandError { } else { write!( f, - "{header} Range formatting isn't supported for notebooks", + "{header} Range formatting is only supported for Python files", + header = "Failed to format:".bold() + ) + } + } + Self::MarkdownExperimental(path) => { + if let Some(path) = path { + write!( + f, + "{header}{path}{colon} Markdown formatting is experimental, enable preview mode.", + header = "Failed to format ".bold(), + path = fs::relativize_path(path).bold(), + colon = ":".bold() + ) + } else { + write!( + f, + "{header} Markdown formatting is experimental, enable preview mode", header = "Failed to format:".bold() ) } @@ -1248,7 +1297,10 @@ mod tests { #[test] fn error_diagnostics() -> anyhow::Result<()> { let path = PathBuf::from("test.py"); - let source_kind = SourceKind::Python("1".to_string()); + let source_kind = SourceKind::Python { + code: "1".to_string(), + is_stub: false, + }; let panic_error = catch_unwind(|| { panic!("Test panic for FormatCommandError"); @@ -1290,7 +1342,7 @@ mod tests { "Cannot write to file", )), ), - FormatCommandError::RangeFormatNotebook(Some(path)), + FormatCommandError::RangeFormatNotSupported(Some(path)), ]; let results = FormatResults::new(&[], FormatMode::Check); @@ -1305,7 +1357,7 @@ mod tests { settings.add_filter(r"(Panicked at) [^:]+:\d+:\d+", "$1 "); let _s = settings.bind_to_scope(); - assert_snapshot!(str::from_utf8(&buf)?, @" + assert_snapshot!(str::from_utf8(&buf)?, @r" io: test.py: Permission denied --> test.py:1:1 @@ -1321,7 +1373,7 @@ mod tests { io: Cannot write to file --> test.py:1:1 - invalid-cli-option: Range formatting isn't supported for notebooks. + invalid-cli-option: Range formatting is only supported for Python files. --> test.py:1:1 panic: Panicked at when checking `test.py`: `Test panic for FormatCommandError` @@ -1347,8 +1399,14 @@ mod tests { expect_formatted: Range, ) { let mr = ModifiedRange::new( - &SourceKind::Python(unformatted.to_string()), - &SourceKind::Python(formatted.to_string()), + &SourceKind::Python { + code: unformatted.to_string(), + is_stub: false, + }, + &SourceKind::Python { + code: formatted.to_string(), + is_stub: false, + }, ); assert_eq!( mr.unformatted, diff --git a/crates/ruff/src/commands/format_stdin.rs b/crates/ruff/src/commands/format_stdin.rs index 015f32fba91206..446ccc0eecaed8 100644 --- a/crates/ruff/src/commands/format_stdin.rs +++ b/crates/ruff/src/commands/format_stdin.rs @@ -53,7 +53,7 @@ pub(crate) fn format_stdin( let source_type = match path.and_then(|path| settings.extension.get(path)) { None => match path.map(SourceType::from).unwrap_or_default() { - SourceType::Python(source_type) => source_type, + source_type @ (SourceType::Python(_) | SourceType::Markdown) => source_type, SourceType::Toml(_) => { if mode.is_write() { parrot_stdin()?; @@ -61,7 +61,7 @@ pub(crate) fn format_stdin( return Ok(ExitStatus::Success); } }, - Some(language) => PySourceType::from(language), + Some(language) => SourceType::Python(PySourceType::from(language)), }; // Format the file. @@ -88,7 +88,7 @@ fn format_source_code( path: Option<&Path>, range: Option, settings: &FormatterSettings, - source_type: PySourceType, + source_type: SourceType, mode: FormatMode, ) -> Result { // Read the source from stdin. @@ -104,7 +104,7 @@ fn format_source_code( }; // Format the source. - let formatted = format_source(&source_kind, source_type, path, settings, range)?; + let formatted = format_source(&source_kind, path, settings, range)?; match &formatted { FormattedSource::Formatted(formatted) => match mode { diff --git a/crates/ruff/src/diagnostics.rs b/crates/ruff/src/diagnostics.rs index ef7623145a6373..8cbc871aea1274 100644 --- a/crates/ruff/src/diagnostics.rs +++ b/crates/ruff/src/diagnostics.rs @@ -234,14 +234,17 @@ pub(crate) fn lint_path( ..Diagnostics::default() }); } - SourceType::Toml(_) => return Ok(Diagnostics::default()), + SourceType::Toml(_) | SourceType::Markdown => return Ok(Diagnostics::default()), SourceType::Python(source_type) => source_type, }, }; // Extract the sources from the file. - let source_kind = match SourceKind::from_path(path, source_type) { - Ok(Some(source_kind)) => source_kind, + let source_kind = match SourceKind::from_path(path, SourceType::Python(source_type)) { + Ok(Some(source_kind)) => match source_kind { + SourceKind::Markdown(_) => return Ok(Diagnostics::default()), // skip linting markdown + _ => source_kind, + }, Ok(None) => return Ok(Diagnostics::default()), Err(err) => { return Ok(Diagnostics::from_source_error(&err, Some(path), settings)); @@ -352,10 +355,10 @@ pub(crate) fn lint_stdin( noqa: flags::Noqa, fix_mode: flags::FixMode, ) -> Result { - let source_type = match path.and_then(|path| settings.linter.extension.get(path)) { + let (source_type, py_source_type) = match path + .and_then(|path| settings.linter.extension.get(path)) + { None => match path.map(SourceType::from).unwrap_or_default() { - SourceType::Python(source_type) => source_type, - SourceType::Toml(source_type) if source_type.is_pyproject() => { if !settings .linter @@ -382,9 +385,13 @@ pub(crate) fn lint_stdin( }); } - SourceType::Toml(_) => return Ok(Diagnostics::default()), + SourceType::Toml(_) | SourceType::Markdown => return Ok(Diagnostics::default()), + source_type @ SourceType::Python(py_source_type) => (source_type, py_source_type), }, - Some(language) => PySourceType::from(language), + Some(language) => { + let py_source_type = PySourceType::from(language); + (SourceType::Python(py_source_type), py_source_type) + } }; // Extract the sources from the file. @@ -410,7 +417,7 @@ pub(crate) fn lint_stdin( settings.unsafe_fixes, &settings.linter, &source_kind, - source_type, + py_source_type, ) { match fix_mode { flags::FixMode::Apply => { @@ -443,7 +450,7 @@ pub(crate) fn lint_stdin( &settings.linter, noqa, &source_kind, - source_type, + py_source_type, ParseSource::None, ); @@ -463,7 +470,7 @@ pub(crate) fn lint_stdin( &settings.linter, noqa, &source_kind, - source_type, + py_source_type, ParseSource::None, ); let transformed = source_kind; diff --git a/crates/ruff/tests/cli/format.rs b/crates/ruff/tests/cli/format.rs index 355f8850089f25..7fe94536137cd3 100644 --- a/crates/ruff/tests/cli/format.rs +++ b/crates/ruff/tests/cli/format.rs @@ -6,7 +6,7 @@ use std::path::Path; use anyhow::Result; use insta_cmd::assert_cmd_snapshot; -use super::{CliTest, tempdir_filter}; +use super::CliTest; #[test] fn default_options() -> Result<()> { @@ -603,14 +603,8 @@ if __name__ == "__main__": #[test] fn output_format_notebook() -> Result<()> { - let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); - let fixtures = crate_root.join("resources").join("test").join("fixtures"); - let path = fixtures.join("unformatted.ipynb"); - - let test = CliTest::with_settings(|_, mut settings| { - settings.add_filter(&tempdir_filter(crate_root.to_str().unwrap()), "CRATE_ROOT/"); - settings - })?; + let test = CliTest::new()?; + let path = test.fixture_path("unformatted.ipynb"); assert_cmd_snapshot!( test.format_command().args(["--isolated", "--preview", "--check"]).arg(path), @@ -1232,16 +1226,11 @@ def say_hy(name: str): #[test] fn test_diff() -> Result<()> { - let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); - let test = CliTest::with_settings(|_, mut settings| { - settings.add_filter(&tempdir_filter(crate_root.to_str().unwrap()), "CRATE_ROOT/"); - settings - })?; - let fixtures = crate_root.join("resources").join("test").join("fixtures"); + let test = CliTest::new()?; let paths = [ - fixtures.join("unformatted.py"), - fixtures.join("formatted.py"), - fixtures.join("unformatted.ipynb"), + test.fixture_path("unformatted.py"), + test.fixture_path("formatted.py"), + test.fixture_path("unformatted.ipynb"), ]; assert_cmd_snapshot!( @@ -1301,14 +1290,8 @@ fn test_diff() -> Result<()> { #[test] fn test_diff_no_change() -> Result<()> { - let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); - let test = CliTest::with_settings(|_, mut settings| { - settings.add_filter(&tempdir_filter(crate_root.to_str().unwrap()), "CRATE_ROOT/"); - settings - })?; - - let fixtures = crate_root.join("resources").join("test").join("fixtures"); - let paths = [fixtures.join("unformatted.py")]; + let test = CliTest::new()?; + let paths = [test.fixture_path("unformatted.py")]; assert_cmd_snapshot!( test.format_command().args(["--isolated", "--diff"]).args(paths), @" @@ -2274,13 +2257,13 @@ fn range_formatting_notebook() -> Result<()> { "nbformat": 4, "nbformat_minor": 5 } -"#), @" +"#), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Failed to format main.ipynb: Range formatting isn't supported for notebooks. + error: Failed to format main.ipynb: Range formatting is only supported for Python files. "); Ok(()) } @@ -2385,3 +2368,98 @@ fn stable_output_format_warning() -> Result<()> { ); Ok(()) } + +#[test] +fn markdown_formatting_preview_disabled() -> Result<()> { + let test = CliTest::new()?; + let unformatted = test.fixture_path("unformatted.md"); + + assert_cmd_snapshot!(test.format_command() + .args(["--isolated", "--no-preview", "--diff"]) + .arg(unformatted), + @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to format CRATE_ROOT/resources/test/fixtures/unformatted.md: Markdown formatting is experimental, enable preview mode. + "); + Ok(()) +} + +#[test] +fn markdown_formatting_preview_enabled() -> Result<()> { + let test = CliTest::new()?; + let unformatted = test.fixture_path("unformatted.md"); + + assert_cmd_snapshot!(test.format_command() + .args(["--isolated", "--preview", "--check"]) + .arg(unformatted), + @r#" + success: false + exit_code: 1 + ----- stdout ----- + unformatted: File would be reformatted + --> CRATE_ROOT/resources/test/fixtures/unformatted.md:1:1 + 1 | This is a markdown document with two fenced code blocks: + 2 | + 3 | ```py + - print( "hello" ) + - def foo(): pass + 4 + print("hello") + 5 + + 6 + + 7 + def foo(): + 8 + pass + 9 | ``` + 10 | + 11 | ```pyi + - print( "hello" ) + - def foo(): pass + 12 + print("hello") + 13 + + 14 + def foo(): + 15 + pass + 16 | ``` + + 1 file would be reformatted + + ----- stderr ----- + "#); + Ok(()) +} + +#[test] +fn markdown_formatting_stdin() -> Result<()> { + let test = CliTest::new()?; + let unformatted = fs::read(test.fixture_path("unformatted.md")).unwrap(); + + assert_cmd_snapshot!(test.format_command() + .args(["--isolated", "--preview", "--stdin-filename", "unformatted.md"]) + .arg("-") + .pass_stdin(unformatted), @r#" + success: true + exit_code: 0 + ----- stdout ----- + This is a markdown document with two fenced code blocks: + + ```py + print("hello") + + + def foo(): + pass + ``` + + ```pyi + print("hello") + + def foo(): + pass + ``` + + ----- stderr ----- + "#); + Ok(()) +} diff --git a/crates/ruff/tests/cli/main.rs b/crates/ruff/tests/cli/main.rs index 879035f7bb2646..cbb69456f2bea0 100644 --- a/crates/ruff/tests/cli/main.rs +++ b/crates/ruff/tests/cli/main.rs @@ -86,6 +86,10 @@ impl CliTest { let mut settings = setup_settings(&project_dir, insta::Settings::clone_current()); settings.add_filter(&tempdir_filter(project_dir.to_str().unwrap()), "[TMP]/"); + settings.add_filter( + &tempdir_filter(Self::crate_root().to_str().unwrap()), + "CRATE_ROOT/", + ); settings.add_filter(r#"\\([\w&&[^nr"]]\w|\s|\.)"#, "/$1"); settings.add_filter(r"(Panicked at) [^:]+:\d+:\d+", "$1 "); settings.add_filter(ruff_linter::VERSION, "[VERSION]"); @@ -167,6 +171,21 @@ impl CliTest { &self.project_dir } + /// Returns the path to the crate root. + pub(crate) fn crate_root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + } + + /// Returns the path to a fixture file inside the crate root. + #[expect(clippy::unused_self)] + pub(crate) fn fixture_path(&self, filename: &str) -> PathBuf { + Self::crate_root() + .join("resources") + .join("test") + .join("fixtures") + .join(filename) + } + /// Creates a pre-configured ruff command for testing. /// /// The command is set up with: diff --git a/crates/ruff_benchmark/benches/linter.rs b/crates/ruff_benchmark/benches/linter.rs index 98ef4083a9fcae..55030dde5f4753 100644 --- a/crates/ruff_benchmark/benches/linter.rs +++ b/crates/ruff_benchmark/benches/linter.rs @@ -83,13 +83,17 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) { assert!(parsed.has_valid_syntax()); let path = case.path(); + let py_source_type = PySourceType::from(path.as_path()); lint_only( &path, None, settings, flags::Noqa::Enabled, - &SourceKind::Python(case.code().to_string()), - PySourceType::from(path.as_path()), + &SourceKind::Python { + code: case.code().to_string(), + is_stub: py_source_type.is_stub(), + }, + py_source_type, ParseSource::Precomputed(parsed), ) }, diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index c9013040554c99..76f567782a30a5 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -1040,6 +1040,9 @@ pub enum DiagnosticId { /// Use of an invalid command-line option. InvalidCliOption, + /// Experimental feature requires preview mode. + PreviewFeature, + /// An internal assumption was violated. /// /// This indicates a bug in the program rather than a user error. @@ -1092,6 +1095,7 @@ impl DiagnosticId { DiagnosticId::DeprecatedSetting => "deprecated-setting", DiagnosticId::Unformatted => "unformatted", DiagnosticId::InvalidCliOption => "invalid-cli-option", + DiagnosticId::PreviewFeature => "preview-feature", DiagnosticId::InternalError => "internal-error", } } diff --git a/crates/ruff_dev/src/print_ast.rs b/crates/ruff_dev/src/print_ast.rs index 7dc1c4e80185d2..c4f79759d653ca 100644 --- a/crates/ruff_dev/src/print_ast.rs +++ b/crates/ruff_dev/src/print_ast.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use anyhow::Result; use ruff_linter::source_kind::SourceKind; -use ruff_python_ast::PySourceType; +use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_parser::{ParseOptions, parse}; #[derive(clap::Args)] @@ -17,12 +17,13 @@ pub(crate) struct Args { pub(crate) fn main(args: &Args) -> Result<()> { let source_type = PySourceType::from(&args.file); - let source_kind = SourceKind::from_path(&args.file, source_type)?.ok_or_else(|| { - anyhow::anyhow!( - "Could not determine source kind for file: {}", - args.file.display() - ) - })?; + let source_kind = SourceKind::from_path(&args.file, SourceType::Python(source_type))? + .ok_or_else(|| { + anyhow::anyhow!( + "Could not determine source kind for file: {}", + args.file.display() + ) + })?; let python_ast = parse(source_kind.source_code(), ParseOptions::from(source_type))?.into_syntax(); println!("{python_ast:#?}"); diff --git a/crates/ruff_dev/src/print_tokens.rs b/crates/ruff_dev/src/print_tokens.rs index 73c7844ccebe25..3e7c89b868c22f 100644 --- a/crates/ruff_dev/src/print_tokens.rs +++ b/crates/ruff_dev/src/print_tokens.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use anyhow::Result; use ruff_linter::source_kind::SourceKind; -use ruff_python_ast::PySourceType; +use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_parser::parse_unchecked_source; #[derive(clap::Args)] @@ -17,12 +17,13 @@ pub(crate) struct Args { pub(crate) fn main(args: &Args) -> Result<()> { let source_type = PySourceType::from(&args.file); - let source_kind = SourceKind::from_path(&args.file, source_type)?.ok_or_else(|| { - anyhow::anyhow!( - "Could not determine source kind for file: {}", - args.file.display() - ) - })?; + let source_kind = SourceKind::from_path(&args.file, SourceType::Python(source_type))? + .ok_or_else(|| { + anyhow::anyhow!( + "Could not determine source kind for file: {}", + args.file.display() + ) + })?; let parsed = parse_unchecked_source(source_kind.source_code(), source_type); for token in parsed.tokens() { println!("{token:#?}"); diff --git a/crates/ruff_linter/src/lib.rs b/crates/ruff_linter/src/lib.rs index 24f0808ee43fd7..fe06603f764ecb 100644 --- a/crates/ruff_linter/src/lib.rs +++ b/crates/ruff_linter/src/lib.rs @@ -60,7 +60,7 @@ pub const RUFF_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); mod tests { use std::path::Path; - use ruff_python_ast::PySourceType; + use ruff_python_ast::{PySourceType, SourceType}; use crate::codes::Rule; use crate::settings::LinterSettings; @@ -77,7 +77,7 @@ mod tests {     Returns: """ "#; - let source_type = PySourceType::Python; + let source_type = SourceType::Python(PySourceType::Python); let rule = Rule::OverIndentation; let source_kind = SourceKind::from_source_code(code.to_string(), source_type) diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index ae9b6b215fa5ca..8e8a85b8461b99 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -1031,7 +1031,10 @@ mod tests { ); let path = Path::new("resources/test/fixtures/semantic_errors").join(path); let contents = std::fs::read_to_string(&path)?; - let source_kind = SourceKind::Python(contents); + let source_kind = SourceKind::Python { + code: contents, + is_stub: false, + }; let diagnostics = test_contents_syntax_errors( &source_kind, @@ -1089,7 +1092,10 @@ mod tests { let snapshot = path.to_string_lossy().to_string(); let path = Path::new("resources/test/fixtures/syntax_errors").join(path); let diagnostics = test_contents_syntax_errors( - &SourceKind::Python(std::fs::read_to_string(&path)?), + &SourceKind::Python { + code: std::fs::read_to_string(&path)?, + is_stub: false, + }, &path, &LinterSettings::for_rule(rule), ); @@ -1211,8 +1217,15 @@ mod tests { let snapshot = format!("disabled_typing_extensions_pyi_{name}"); let path = Path::new(".pyi"); let contents = dedent(contents); - let diagnostics = - test_contents(&SourceKind::Python(contents.into_owned()), path, settings).0; + let diagnostics = test_contents( + &SourceKind::Python { + code: contents.into_owned(), + is_stub: true, + }, + path, + settings, + ) + .0; assert_diagnostics!(snapshot, diagnostics); } } diff --git a/crates/ruff_linter/src/rules/pyflakes/mod.rs b/crates/ruff_linter/src/rules/pyflakes/mod.rs index e1cc8ea9358033..dc91e21a18af7e 100644 --- a/crates/ruff_linter/src/rules/pyflakes/mod.rs +++ b/crates/ruff_linter/src/rules/pyflakes/mod.rs @@ -264,7 +264,10 @@ mod tests { )] fn f401_preview_first_party_submodule(contents: &str, snapshot: &str) { let diagnostics = test_contents( - &SourceKind::Python(dedent(contents).to_string()), + &SourceKind::Python { + code: dedent(contents).to_string(), + is_stub: false, + }, Path::new("f401_preview_first_party_submodule/__init__.py"), &LinterSettings { preview: PreviewMode::Enabled, @@ -563,7 +566,10 @@ mod tests { )] fn f401_preview_refined_submodule_handling(contents: &str, snapshot: &str) { let diagnostics = test_contents( - &SourceKind::Python(dedent(contents).to_string()), + &SourceKind::Python { + code: dedent(contents).to_string(), + is_stub: false, + }, Path::new("f401_preview_submodule.py"), &LinterSettings { preview: PreviewMode::Enabled, @@ -939,7 +945,10 @@ mod tests { fn flakes(contents: &str, expected: &[Rule]) { let contents = dedent(contents); let source_type = PySourceType::default(); - let source_kind = SourceKind::Python(contents.to_string()); + let source_kind = SourceKind::Python { + code: contents.to_string(), + is_stub: source_type.is_stub(), + }; let settings = LinterSettings::for_rules(Linter::Pyflakes.rules()); let target_version = settings.unresolved_target_version; let options = diff --git a/crates/ruff_linter/src/source_kind.rs b/crates/ruff_linter/src/source_kind.rs index a146d2db137101..56d8911670eb23 100644 --- a/crates/ruff_linter/src/source_kind.rs +++ b/crates/ruff_linter/src/source_kind.rs @@ -9,7 +9,7 @@ use thiserror::Error; use ruff_diagnostics::SourceMap; use ruff_notebook::{Cell, Notebook, NotebookError}; -use ruff_python_ast::PySourceType; +use ruff_python_ast::{PySourceType, SourceType}; use colored::Colorize; @@ -18,10 +18,12 @@ use crate::text_helpers::ShowNonprinting; #[derive(Clone, Debug, PartialEq)] pub enum SourceKind { - /// The source contains Python source code. - Python(String), + /// The source contains Python source code, and whether it's a stub. + Python { code: String, is_stub: bool }, /// The source contains a Jupyter notebook. IpyNotebook(Box), + /// The source contains Markdown text. + Markdown(String), } impl SourceKind { @@ -32,28 +34,54 @@ impl SourceKind { pub fn as_ipy_notebook(&self) -> Option<&Notebook> { match self { SourceKind::IpyNotebook(notebook) => Some(notebook), - SourceKind::Python(_) => None, + SourceKind::Python { .. } => None, + SourceKind::Markdown(_) => None, } } pub fn as_python(&self) -> Option<&str> { match self { - SourceKind::Python(code) => Some(code), + SourceKind::Python { code, .. } => Some(code), + SourceKind::Markdown(_) => None, + SourceKind::IpyNotebook(_) => None, + } + } + + pub fn as_markdown(&self) -> Option<&str> { + match self { + SourceKind::Markdown(code) => Some(code), + SourceKind::Python { .. } => None, SourceKind::IpyNotebook(_) => None, } } pub fn expect_python(self) -> String { match self { - SourceKind::Python(code) => code, - SourceKind::IpyNotebook(_) => panic!("expected python code"), + SourceKind::Python { code, is_stub: _ } => code, + _ => panic!("expected python code"), } } pub fn expect_ipy_notebook(self) -> Notebook { match self { SourceKind::IpyNotebook(notebook) => *notebook, - SourceKind::Python(_) => panic!("expected ipy notebook"), + _ => panic!("expected ipy notebook"), + } + } + + pub fn expect_markdown(self) -> String { + match self { + SourceKind::Markdown(code) => code, + _ => panic!("expected markdown text"), + } + } + + pub fn py_source_type(&self) -> PySourceType { + match self { + Self::IpyNotebook(_) => PySourceType::Ipynb, + Self::Python { is_stub: true, .. } => PySourceType::Stub, + Self::Python { is_stub: false, .. } => PySourceType::Python, + Self::Markdown(_) => PySourceType::Python, } } @@ -65,45 +93,65 @@ impl SourceKind { cloned.update(source_map, new_source); SourceKind::IpyNotebook(cloned) } - SourceKind::Python(_) => SourceKind::Python(new_source), + SourceKind::Python { is_stub, .. } => SourceKind::Python { + code: new_source, + is_stub: *is_stub, + }, + SourceKind::Markdown(_) => SourceKind::Markdown(new_source), } } /// Returns the Python source code for this source kind. pub fn source_code(&self) -> &str { match self { - SourceKind::Python(source) => source, + SourceKind::Python { code, .. } | SourceKind::Markdown(code) => code, SourceKind::IpyNotebook(notebook) => notebook.source_code(), } } - /// Read the [`SourceKind`] from the given path. Returns `None` if the source is not a Python - /// source file. - pub fn from_path(path: &Path, source_type: PySourceType) -> Result, SourceError> { - if source_type.is_ipynb() { - let notebook = Notebook::from_path(path)?; - Ok(notebook - .is_python_notebook() - .then_some(Self::IpyNotebook(Box::new(notebook)))) - } else { - let contents = std::fs::read_to_string(path)?; - Ok(Some(Self::Python(contents))) + /// Read the [`SourceKind`] from the given path. Returns `None` if the source is a TOML file. + pub fn from_path(path: &Path, source_type: SourceType) -> Result, SourceError> { + match source_type { + SourceType::Python(PySourceType::Ipynb) => { + let notebook = Notebook::from_path(path)?; + Ok(notebook + .is_python_notebook() + .then_some(Self::IpyNotebook(Box::new(notebook)))) + } + SourceType::Python(py_source_type) => { + let contents = std::fs::read_to_string(path)?; + Ok(Some(Self::Python { + code: contents, + is_stub: py_source_type.is_stub(), + })) + } + SourceType::Markdown => { + let contents = std::fs::read_to_string(path)?; + Ok(Some(Self::Markdown(contents))) + } + SourceType::Toml(_) => Ok(None), } } - /// Read the [`SourceKind`] from the given source code. Returns `None` if the source is not - /// Python source code. + /// Read the [`SourceKind`] from the given source code. Returns `None` if the source is + /// a TOML file. pub fn from_source_code( source_code: String, - source_type: PySourceType, + source_type: SourceType, ) -> Result, SourceError> { - if source_type.is_ipynb() { - let notebook = Notebook::from_source_code(&source_code)?; - Ok(notebook - .is_python_notebook() - .then_some(Self::IpyNotebook(Box::new(notebook)))) - } else { - Ok(Some(Self::Python(source_code))) + match source_type { + SourceType::Python(PySourceType::Ipynb) => { + let notebook = Notebook::from_source_code(&source_code)?; + Ok(notebook + .is_python_notebook() + .then_some(Self::IpyNotebook(Box::new(notebook)))) + } + SourceType::Python(py_source_type) => Ok(Some(Self::Python { + code: source_code, + is_stub: py_source_type.is_stub(), + })), + SourceType::Toml(_) => Ok(None), + SourceType::Markdown => Ok(Some(Self::Markdown(source_code))), } } @@ -112,8 +160,8 @@ impl SourceKind { /// For Jupyter notebooks, this will write out the notebook as JSON. pub fn write(&self, writer: &mut dyn Write) -> Result<(), SourceError> { match self { - SourceKind::Python(source) => { - writer.write_all(source.as_bytes())?; + SourceKind::Python { code, .. } | SourceKind::Markdown(code) => { + writer.write_all(code.as_bytes())?; Ok(()) } SourceKind::IpyNotebook(notebook) => { @@ -132,14 +180,20 @@ impl SourceKind { path: Option<&'a Path>, ) -> Option> { match (self, other) { - (SourceKind::Python(src), SourceKind::Python(dst)) => Some(SourceKindDiff { - kind: DiffKind::Python(src, dst), - path, - }), + (SourceKind::Python { code: src, .. }, SourceKind::Python { code: dst, .. }) => { + Some(SourceKindDiff { + kind: DiffKind::Text(src, dst), + path, + }) + } (SourceKind::IpyNotebook(src), SourceKind::IpyNotebook(dst)) => Some(SourceKindDiff { kind: DiffKind::IpyNotebook(src, dst), path, }), + (SourceKind::Markdown(src), SourceKind::Markdown(dst)) => Some(SourceKindDiff { + kind: DiffKind::Text(src, dst), + path, + }), _ => None, } } @@ -154,7 +208,7 @@ pub struct SourceKindDiff<'a> { impl std::fmt::Display for SourceKindDiff<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.kind { - DiffKind::Python(original, modified) => { + DiffKind::Text(original, modified) => { let mut diff = CodeDiff::new(original, modified); let relative_path = self.path.map(fs::relativize_path); @@ -220,7 +274,7 @@ impl std::fmt::Display for SourceKindDiff<'_> { #[derive(Debug, Clone, Copy)] enum DiffKind<'a> { - Python(&'a str, &'a str), + Text(&'a str, &'a str), IpyNotebook(&'a Notebook, &'a Notebook), } diff --git a/crates/ruff_linter/src/test.rs b/crates/ruff_linter/src/test.rs index fc3d79b4f6cc01..ce6d56018ca777 100644 --- a/crates/ruff_linter/src/test.rs +++ b/crates/ruff_linter/src/test.rs @@ -16,7 +16,7 @@ use ruff_db::diagnostic::{ use ruff_notebook::Notebook; #[cfg(not(fuzzing))] use ruff_notebook::NotebookError; -use ruff_python_ast::PySourceType; +use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_codegen::Stylist; use ruff_python_index::Indexer; use ruff_python_parser::{ParseError, ParseOptions}; @@ -127,7 +127,7 @@ pub(crate) fn test_path( settings: &LinterSettings, ) -> Result> { let path = test_resource_path("fixtures").join(path); - let source_type = PySourceType::from(&path); + let source_type = SourceType::Python(PySourceType::from(&path)); let source_kind = SourceKind::from_path(path.as_ref(), source_type)?.expect("valid source"); Ok(test_contents(&source_kind, &path, settings).0) } @@ -197,7 +197,15 @@ pub(crate) fn assert_notebook_path( pub fn test_snippet(contents: &str, settings: &LinterSettings) -> Vec { let path = Path::new(""); let contents = dedent(contents); - test_contents(&SourceKind::Python(contents.into_owned()), path, settings).0 + test_contents( + &SourceKind::Python { + code: contents.into_owned(), + is_stub: false, + }, + path, + settings, + ) + .0 } thread_local! { diff --git a/crates/ruff_markdown/Cargo.toml b/crates/ruff_markdown/Cargo.toml new file mode 100644 index 00000000000000..19fd48c8d64546 --- /dev/null +++ b/crates/ruff_markdown/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ruff_markdown" +version = "0.0.0" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[dependencies] +ruff_python_ast = { workspace = true } +ruff_python_formatter = { workspace = true } +ruff_python_trivia = { workspace = true } +ruff_workspace = { workspace = true } + +insta = { workspace = true } +regex = { workspace = true } + +[lints] +workspace = true diff --git a/crates/ruff_markdown/src/lib.rs b/crates/ruff_markdown/src/lib.rs new file mode 100644 index 00000000000000..79d2b0a62ea958 --- /dev/null +++ b/crates/ruff_markdown/src/lib.rs @@ -0,0 +1,190 @@ +use std::{path::Path, sync::LazyLock}; + +use regex::Regex; +use ruff_python_ast::PySourceType; +use ruff_python_formatter::format_module_source; +use ruff_python_trivia::textwrap::{dedent, indent}; +use ruff_workspace::FormatterSettings; + +#[derive(Debug, PartialEq, Eq)] +pub enum MarkdownResult { + Formatted(String), + Unchanged, +} + +// TODO: account for ~~~ and arbitrary length code fences +// TODO: support code blocks nested inside block quotes, etc +static MARKDOWN_CODE_BLOCK: LazyLock = LazyLock::new(|| { + // adapted from blacken-docs + // https://github.com/adamchainz/blacken-docs/blob/fb107c1dce25f9206e29297aaa1ed7afc2980a5a/src/blacken_docs/__init__.py#L17 + Regex::new( + r"(?imsx) + (? + ^(?\ *)```[^\S\r\n]* + (?(?:python|py|python3|py3|pyi)?) + (?:\ .*?)?\n + ) + (?.*?) + (? + ^\ *```[^\S\r\n]*$ + ) + ", + ) + .unwrap() +}); + +pub fn format_code_blocks( + source: &str, + path: Option<&Path>, + settings: &FormatterSettings, +) -> MarkdownResult { + let mut changed = false; + let mut formatted = String::with_capacity(source.len()); + let mut last_match = 0; + + for capture in MARKDOWN_CODE_BLOCK.captures_iter(source) { + let (_, [before, code_indent, language, code, after]) = capture.extract(); + + let py_source_type = PySourceType::from_extension(language); + let unformatted_code = dedent(code); + let options = settings.to_format_options(py_source_type, &unformatted_code, path); + + // Using `Printed::into_code` requires adding `ruff_formatter` as a direct dependency, and I suspect that Rust can optimize the closure away regardless. + #[expect(clippy::redundant_closure_for_method_calls)] + let formatted_code = + format_module_source(&unformatted_code, options).map(|formatted| formatted.into_code()); + + if let Ok(formatted_code) = formatted_code { + if formatted_code.len() != unformatted_code.len() || formatted_code != *unformatted_code + { + let m = capture.get_match(); + formatted.push_str(&source[last_match..m.start()]); + + let indented_code = indent(&formatted_code, code_indent); + // otherwise I need to deal with a result from write! + #[expect(clippy::format_push_string)] + formatted.push_str(&format!("{before}{indented_code}{after}")); + + last_match = m.end(); + changed = true; + } + } + } + + if changed { + formatted.push_str(&source[last_match..]); + MarkdownResult::Formatted(formatted) + } else { + MarkdownResult::Unchanged + } +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + use ruff_workspace::FormatterSettings; + + use crate::{MarkdownResult, format_code_blocks}; + + impl std::fmt::Display for MarkdownResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Formatted(source) => write!(f, "{source}"), + Self::Unchanged => write!(f, "Unchanged"), + } + } + } + + #[test] + fn format_code_blocks_basic() { + let code = r#" +This is poorly formatted code: + +```py +print( "hello" ) +``` + +More text. + "#; + assert_snapshot!( + format_code_blocks(code, None, &FormatterSettings::default()), + @r#" + This is poorly formatted code: + + ```py + print("hello") + ``` + + More text. + "# + ); + } + + #[test] + fn format_code_blocks_unchanged() { + let code = r#" +This is well formatted code: + +```py +print("hello") +``` + +More text. + "#; + assert_snapshot!( + format_code_blocks(code, None, &FormatterSettings::default()), + @"Unchanged"); + } + + #[test] + fn format_code_blocks_syntax_error() { + let code = r#" +This is well formatted code: + +```py +print "hello" +``` + +More text. + "#; + assert_snapshot!( + format_code_blocks(code, None, &FormatterSettings::default()), + @"Unchanged"); + } + + #[test] + fn format_code_blocks_unlabeled_python() { + let code = r#" +This is poorly formatted code: + +``` +print( "hello" ) +``` + "#; + assert_snapshot!( + format_code_blocks(code, None, &FormatterSettings::default()), + @r#" + This is poorly formatted code: + + ``` + print("hello") + ``` + "#); + } + + #[test] + fn format_code_blocks_unlabeled_rust() { + let code = r#" +This is poorly formatted code: + +``` +fn (foo: &str) -> &str { + foo +} +``` + "#; + assert_snapshot!( + format_code_blocks(code, None, &FormatterSettings::default()), + @"Unchanged"); + } +} diff --git a/crates/ruff_python_ast/src/lib.rs b/crates/ruff_python_ast/src/lib.rs index 31c4cd019d62db..ace52e83f9d18c 100644 --- a/crates/ruff_python_ast/src/lib.rs +++ b/crates/ruff_python_ast/src/lib.rs @@ -43,6 +43,8 @@ pub enum SourceType { Python(PySourceType), /// The file contains TOML. Toml(TomlSourceType), + /// The file contains Markdown. + Markdown, } impl Default for SourceType { @@ -59,6 +61,7 @@ impl> From

for SourceType { Some(filename) if filename == "poetry.lock" => Self::Toml(TomlSourceType::Poetry), _ => match path.as_ref().extension() { Some(ext) if ext == "toml" => Self::Toml(TomlSourceType::Unrecognized), + Some(ext) if ext == "md" => Self::Markdown, _ => Self::Python(PySourceType::from(path)), }, } diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs index 32933aa4e6e176..3fcf7d1db02a31 100644 --- a/crates/ruff_server/src/session/index.rs +++ b/crates/ruff_server/src/session/index.rs @@ -564,9 +564,10 @@ impl DocumentQuery { /// Generate a source kind used by the linter. pub(crate) fn make_source_kind(&self) -> ruff_linter::source_kind::SourceKind { match self { - Self::Text { document, .. } => { - ruff_linter::source_kind::SourceKind::Python(document.contents().to_string()) - } + Self::Text { document, .. } => ruff_linter::source_kind::SourceKind::Python { + code: document.contents().to_string(), + is_stub: ruff_python_ast::PySourceType::from(self.virtual_file_path()).is_stub(), + }, Self::Notebook { notebook, .. } => { ruff_linter::source_kind::SourceKind::ipy_notebook(notebook.make_ruff_notebook()) } diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 785c2095cb96e8..86ed523e7f9e81 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -212,7 +212,10 @@ impl Workspace { let source_type = PySourceType::default(); // TODO(dhruvmanila): Support Jupyter Notebooks - let source_kind = SourceKind::Python(contents.to_string()); + let source_kind = SourceKind::Python { + code: contents.to_string(), + is_stub: source_type.is_stub(), + }; // Use the unresolved version because we don't have a file path. let target_version = self.settings.linter.unresolved_target_version; diff --git a/fuzz/fuzz_targets/ruff_formatter_validity.rs b/fuzz/fuzz_targets/ruff_formatter_validity.rs index e65c2466fde6b4..ed64be058d94f5 100644 --- a/fuzz/fuzz_targets/ruff_formatter_validity.rs +++ b/fuzz/fuzz_targets/ruff_formatter_validity.rs @@ -32,7 +32,10 @@ fn do_fuzz(case: &[u8]) -> Corpus { None, linter_settings, Noqa::Enabled, - &SourceKind::Python(code.to_string()), + &SourceKind::Python { + code: code.to_string(), + is_stub: false, + }, PySourceType::Python, ParseSource::None, ); @@ -57,7 +60,10 @@ fn do_fuzz(case: &[u8]) -> Corpus { None, linter_settings, Noqa::Enabled, - &SourceKind::Python(formatted.clone()), + &SourceKind::Python { + code: formatted.clone(), + is_stub: false, + }, PySourceType::Python, ParseSource::None, );