From 759bd387ca46e91b20dafa6e77510403947121ec Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Jan 2024 20:06:49 -0500 Subject: [PATCH] Add --extension support to the formatter --- crates/ruff_cli/src/args.rs | 14 ++-- crates/ruff_cli/src/commands/format.rs | 16 +++-- crates/ruff_cli/src/commands/format_stdin.rs | 19 ++++-- crates/ruff_cli/src/diagnostics.rs | 26 +++---- crates/ruff_cli/tests/format.rs | 70 +++++++++++++++++++ crates/ruff_cli/tests/lint.rs | 72 ++++++++++++++++++++ crates/ruff_linter/src/settings/mod.rs | 2 +- crates/ruff_linter/src/settings/types.rs | 7 +- crates/ruff_workspace/src/configuration.rs | 36 ++++++---- crates/ruff_workspace/src/settings.rs | 6 +- docs/configuration.md | 4 ++ 11 files changed, 221 insertions(+), 51 deletions(-) diff --git a/crates/ruff_cli/src/args.rs b/crates/ruff_cli/src/args.rs index 6c5ed14456ab9..4276fb0cc93a9 100644 --- a/crates/ruff_cli/src/args.rs +++ b/crates/ruff_cli/src/args.rs @@ -290,6 +290,10 @@ pub struct CheckCommand { /// The name of the file when passing it through stdin. #[arg(long, help_heading = "Miscellaneous")] pub stdin_filename: Option, + /// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For + /// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`. + #[arg(long, value_delimiter = ',')] + pub extension: Option>, /// Exit with status code "0", even upon detecting lint violations. #[arg( short, @@ -352,9 +356,6 @@ pub struct CheckCommand { conflicts_with = "watch", )] pub show_settings: bool, - /// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). - #[arg(long, value_delimiter = ',', hide = true)] - pub extension: Option>, /// Dev-only argument to show fixes #[arg(long, hide = true)] pub ecosystem_ci: bool, @@ -423,6 +424,10 @@ pub struct FormatCommand { /// The name of the file when passing it through stdin. #[arg(long, help_heading = "Miscellaneous")] pub stdin_filename: Option, + /// List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For + /// example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb`. + #[arg(long, value_delimiter = ',')] + pub extension: Option>, /// The minimum Python version that should be supported. #[arg(long, value_enum)] pub target_version: Option, @@ -571,6 +576,7 @@ impl FormatCommand { force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude), target_version: self.target_version, cache_dir: self.cache_dir, + extension: self.extension, // Unsupported on the formatter CLI, but required on `Overrides`. ..CliOverrides::default() @@ -739,7 +745,7 @@ impl ConfigurationTransformer for CliOverrides { config.target_version = Some(*target_version); } if let Some(extension) = &self.extension { - config.lint.extension = Some(extension.clone().into_iter().collect()); + config.extension = Some(extension.iter().cloned().collect()); } config diff --git a/crates/ruff_cli/src/commands/format.rs b/crates/ruff_cli/src/commands/format.rs index c9287e459c431..f8ecadaf3f24f 100644 --- a/crates/ruff_cli/src/commands/format.rs +++ b/crates/ruff_cli/src/commands/format.rs @@ -106,13 +106,19 @@ pub(crate) fn format( match entry { Ok(resolved_file) => { let path = resolved_file.path(); - let SourceType::Python(source_type) = SourceType::from(&path) else { - // Ignore any non-Python files. - return None; - }; - 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), + }; + // Ignore files that are excluded from formatting if (settings.file_resolver.force_exclude || !resolved_file.is_root()) && match_exclusion( diff --git a/crates/ruff_cli/src/commands/format_stdin.rs b/crates/ruff_cli/src/commands/format_stdin.rs index 33fa5160424fe..41695ae8b5443 100644 --- a/crates/ruff_cli/src/commands/format_stdin.rs +++ b/crates/ruff_cli/src/commands/format_stdin.rs @@ -53,16 +53,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R } let path = cli.stdin_filename.as_deref(); + let settings = &resolver.base_settings().formatter; - let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else { - if mode.is_write() { - parrot_stdin()?; - } - return Ok(ExitStatus::Success); + 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, + SourceType::Toml(_) => { + if mode.is_write() { + parrot_stdin()?; + } + return Ok(ExitStatus::Success); + } + }, + Some(language) => PySourceType::from(language), }; // Format the file. - match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) { + match format_source_code(path, settings, source_type, mode) { Ok(result) => match mode { FormatMode::Write => Ok(ExitStatus::Success), FormatMode::Check | FormatMode::Diff => { diff --git a/crates/ruff_cli/src/diagnostics.rs b/crates/ruff_cli/src/diagnostics.rs index ad9913f083f8b..58d26e894ad14 100644 --- a/crates/ruff_cli/src/diagnostics.rs +++ b/crates/ruff_cli/src/diagnostics.rs @@ -17,7 +17,7 @@ use ruff_linter::logging::DisplayParseError; use ruff_linter::message::Message; use ruff_linter::pyproject_toml::lint_pyproject_toml; use ruff_linter::registry::AsRule; -use ruff_linter::settings::types::{ExtensionMapping, UnsafeFixes}; +use ruff_linter::settings::types::UnsafeFixes; use ruff_linter::settings::{flags, LinterSettings}; use ruff_linter::source_kind::{SourceError, SourceKind}; use ruff_linter::{fs, IOError, SyntaxError}; @@ -179,11 +179,6 @@ impl AddAssign for FixMap { } } -fn override_source_type(path: Option<&Path>, extension: &ExtensionMapping) -> Option { - let ext = path?.extension()?.to_str()?; - extension.get(ext).map(PySourceType::from) -} - /// Lint the source code at the given `Path`. pub(crate) fn lint_path( path: &Path, @@ -228,7 +223,7 @@ pub(crate) fn lint_path( debug!("Checking: {}", path.display()); - let source_type = match override_source_type(Some(path), &settings.extension) { + let source_type = match settings.extension.get(path).map(PySourceType::from) { Some(source_type) => source_type, None => match SourceType::from(path) { SourceType::Toml(TomlSourceType::Pyproject) => { @@ -398,15 +393,14 @@ pub(crate) fn lint_stdin( fix_mode: flags::FixMode, ) -> Result { // TODO(charlie): Support `pyproject.toml`. - let source_type = if let Some(source_type) = - override_source_type(path, &settings.linter.extension) - { - source_type - } else { - let SourceType::Python(source_type) = path.map(SourceType::from).unwrap_or_default() else { - return Ok(Diagnostics::default()); - }; - source_type + let 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(_) => { + return Ok(Diagnostics::default()); + } + }, + Some(language) => PySourceType::from(language), }; // Extract the sources from the file. diff --git a/crates/ruff_cli/tests/format.rs b/crates/ruff_cli/tests/format.rs index 232635b8e3047..d7a255fb555ac 100644 --- a/crates/ruff_cli/tests/format.rs +++ b/crates/ruff_cli/tests/format.rs @@ -1468,3 +1468,73 @@ fn test_notebook_trailing_semicolon() { ----- stderr ----- "###); } + +#[test] +fn extension() -> Result<()> { + let tempdir = TempDir::new()?; + + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +include = ["*.ipy"] +"#, + )?; + + fs::write( + tempdir.path().join("main.ipy"), + r#" +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", + "metadata": {}, + "outputs": [], + "source": [ + "x=1" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .current_dir(tempdir.path()) + .arg("format") + .arg("--no-cache") + .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) + .args(["--extension", "ipy:ipynb"]) + .arg("."), @r###" + success: true + exit_code: 0 + ----- stdout ----- + 1 file reformatted + + ----- stderr ----- + "###); + Ok(()) +} diff --git a/crates/ruff_cli/tests/lint.rs b/crates/ruff_cli/tests/lint.rs index 5dfbd56f83c55..7787c9a234ec6 100644 --- a/crates/ruff_cli/tests/lint.rs +++ b/crates/ruff_cli/tests/lint.rs @@ -436,3 +436,75 @@ ignore = ["D203", "D212"] "###); Ok(()) } + +#[test] +fn extension() -> Result<()> { + let tempdir = TempDir::new()?; + + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +include = ["*.ipy"] +"#, + )?; + + fs::write( + tempdir.path().join("main.ipy"), + r#" +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ad6f36d9-4b7d-4562-8d00-f15a0f1fbb6d", + "metadata": {}, + "outputs": [], + "source": [ + "import os" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .current_dir(tempdir.path()) + .arg("check") + .args(STDIN_BASE_OPTIONS) + .args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()]) + .args(["--extension", "ipy:ipynb"]) + .arg("."), @r###" + success: false + exit_code: 1 + ----- stdout ----- + main.ipy:cell 1:1:8: F401 [*] `os` imported but unused + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "###); + Ok(()) +} diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index 66214a3a65201..a1d34de5b1b86 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -41,6 +41,7 @@ pub mod types; #[derive(Debug, CacheKey)] pub struct LinterSettings { pub exclude: FilePatternSet, + pub extension: ExtensionMapping, pub project_root: PathBuf, pub rules: RuleTable, @@ -50,7 +51,6 @@ pub struct LinterSettings { pub target_version: PythonVersion, pub preview: PreviewMode, pub explicit_preview_rules: bool, - pub extension: ExtensionMapping, // Rule-specific settings pub allowed_confusables: FxHashSet, diff --git a/crates/ruff_linter/src/settings/types.rs b/crates/ruff_linter/src/settings/types.rs index ed0e83e5c64f7..1ade2f5068c6d 100644 --- a/crates/ruff_linter/src/settings/types.rs +++ b/crates/ruff_linter/src/settings/types.rs @@ -388,9 +388,10 @@ pub struct ExtensionMapping { } impl ExtensionMapping { - /// Return the [`Language`] for the given extension. - pub fn get(&self, extension: &str) -> Option { - self.mapping.get(extension).copied() + /// Return the [`Language`] for the given file. + pub fn get(&self, path: &Path) -> Option { + let ext = path.extension()?.to_str()?; + self.mapping.get(ext).copied() } } diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 5325e9ba7260d..6430f8d481b02 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -117,6 +117,7 @@ pub struct Configuration { pub output_format: Option, pub preview: Option, pub required_version: Option, + pub extension: Option, pub show_fixes: Option, pub show_source: Option, @@ -174,6 +175,7 @@ impl Configuration { let formatter = FormatterSettings { exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?, + extension: self.extension.clone().unwrap_or_default(), preview: format_preview, target_version: match target_version { PythonVersion::Py37 => ruff_python_formatter::PythonVersion::Py37, @@ -241,7 +243,7 @@ impl Configuration { linter: LinterSettings { rules: lint.as_rule_table(lint_preview), exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?, - extension: lint.extension.unwrap_or_default(), + extension: self.extension.unwrap_or_default(), preview: lint_preview, target_version, project_root: project_root.to_path_buf(), @@ -496,6 +498,9 @@ impl Configuration { .map(|src| resolve_src(&src, project_root)) .transpose()?, target_version: options.target_version, + // `--extension` is a hidden command-line argument that isn't supported in configuration + // files at present. + extension: None, lint: LintConfiguration::from_options(lint, project_root)?, format: FormatConfiguration::from_options( @@ -538,6 +543,7 @@ impl Configuration { src: self.src.or(config.src), target_version: self.target_version.or(config.target_version), preview: self.preview.or(config.preview), + extension: self.extension.or(config.extension), lint: self.lint.combine(config.lint), format: self.format.combine(config.format), @@ -549,7 +555,6 @@ impl Configuration { pub struct LintConfiguration { pub exclude: Option>, pub preview: Option, - pub extension: Option, // Rule selection pub extend_per_file_ignores: Vec, @@ -616,9 +621,6 @@ impl LintConfiguration { .chain(options.common.extend_unfixable.into_iter().flatten()) .collect(); Ok(LintConfiguration { - // `--extension` is a hidden command-line argument that isn't supported in configuration - // files at present. - extension: None, exclude: options.exclude.map(|paths| { paths .into_iter() @@ -954,7 +956,6 @@ impl LintConfiguration { Self { exclude: self.exclude.or(config.exclude), preview: self.preview.or(config.preview), - extension: self.extension.or(config.extension), rule_selections: config .rule_selections .into_iter() @@ -1031,6 +1032,7 @@ impl LintConfiguration { pub struct FormatConfiguration { pub exclude: Option>, pub preview: Option, + pub extension: Option, pub indent_style: Option, pub quote_style: Option, @@ -1044,6 +1046,9 @@ impl FormatConfiguration { #[allow(clippy::needless_pass_by_value)] pub fn from_options(options: FormatOptions, project_root: &Path) -> Result { Ok(Self { + // `--extension` is a hidden command-line argument that isn't supported in configuration + // files at present. + extension: None, exclude: options.exclude.map(|paths| { paths .into_iter() @@ -1077,18 +1082,19 @@ impl FormatConfiguration { #[must_use] #[allow(clippy::needless_pass_by_value)] - pub fn combine(self, other: Self) -> Self { + pub fn combine(self, config: Self) -> Self { Self { - exclude: self.exclude.or(other.exclude), - preview: self.preview.or(other.preview), - indent_style: self.indent_style.or(other.indent_style), - quote_style: self.quote_style.or(other.quote_style), - magic_trailing_comma: self.magic_trailing_comma.or(other.magic_trailing_comma), - line_ending: self.line_ending.or(other.line_ending), - docstring_code_format: self.docstring_code_format.or(other.docstring_code_format), + exclude: self.exclude.or(config.exclude), + preview: self.preview.or(config.preview), + extension: self.extension.or(config.extension), + indent_style: self.indent_style.or(config.indent_style), + quote_style: self.quote_style.or(config.quote_style), + magic_trailing_comma: self.magic_trailing_comma.or(config.magic_trailing_comma), + line_ending: self.line_ending.or(config.line_ending), + docstring_code_format: self.docstring_code_format.or(config.docstring_code_format), docstring_code_line_width: self .docstring_code_line_width - .or(other.docstring_code_line_width), + .or(config.docstring_code_line_width), } } } diff --git a/crates/ruff_workspace/src/settings.rs b/crates/ruff_workspace/src/settings.rs index 446cc95173bd0..3e2560285a5b0 100644 --- a/crates/ruff_workspace/src/settings.rs +++ b/crates/ruff_workspace/src/settings.rs @@ -1,7 +1,9 @@ use path_absolutize::path_dedot; use ruff_cache::cache_dir; use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth}; -use ruff_linter::settings::types::{FilePattern, FilePatternSet, SerializationFormat, UnsafeFixes}; +use ruff_linter::settings::types::{ + ExtensionMapping, FilePattern, FilePatternSet, SerializationFormat, UnsafeFixes, +}; use ruff_linter::settings::LinterSettings; use ruff_macros::CacheKey; use ruff_python_ast::PySourceType; @@ -116,6 +118,7 @@ impl FileResolverSettings { #[derive(CacheKey, Clone, Debug)] pub struct FormatterSettings { pub exclude: FilePatternSet, + pub extension: ExtensionMapping, pub preview: PreviewMode, pub target_version: ruff_python_formatter::PythonVersion, @@ -177,6 +180,7 @@ impl Default for FormatterSettings { Self { exclude: FilePatternSet::default(), + extension: ExtensionMapping::default(), target_version: default_options.target_version(), preview: PreviewMode::Disabled, line_width: default_options.line_width(), diff --git a/docs/configuration.md b/docs/configuration.md index ced72b235fe3f..39c12e331b3a6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -528,6 +528,8 @@ Options: Enable preview mode; checks will include unstable rules and fixes. Use `--no-preview` to disable --config Path to the `pyproject.toml` or `ruff.toml` file to use for configuration + --extension + List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb` --statistics Show counts for every rule with at least one violation --add-noqa @@ -604,6 +606,8 @@ Options: Avoid writing any formatted files back; instead, exit with a non-zero status code and the difference between the current file and how the formatted file would look like --config Path to the `pyproject.toml` or `ruff.toml` file to use for configuration + --extension + List of mappings from file extension to language (one of ["python", "ipynb", "pyi"]). For example, to treat `.ipy` files as IPython notebooks, use `--extension ipy:ipynb` --target-version The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311, py312] --preview