Add --extension support to the formatter (#9483)

## Summary

We added `--extension` to `ruff check`, but it's equally applicable to
`ruff format`.

Closes https://github.com/astral-sh/ruff/issues/9482.

Resolves https://github.com/astral-sh/ruff/discussions/9481.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2024-01-12 13:53:25 -05:00 committed by GitHub
parent d16c4a2d25
commit 3261d16e61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 221 additions and 51 deletions

View file

@ -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<PathBuf>,
/// 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<Vec<ExtensionPair>>,
/// 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<Vec<ExtensionPair>>,
/// 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<PathBuf>,
/// 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<Vec<ExtensionPair>>,
/// The minimum Python version that should be supported.
#[arg(long, value_enum)]
pub target_version: Option<PythonVersion>,
@ -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

View file

@ -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(

View file

@ -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 => {

View file

@ -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<PySourceType> {
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<Diagnostics> {
// 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.

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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<char>,

View file

@ -388,9 +388,10 @@ pub struct ExtensionMapping {
}
impl ExtensionMapping {
/// Return the [`Language`] for the given extension.
pub fn get(&self, extension: &str) -> Option<Language> {
self.mapping.get(extension).copied()
/// Return the [`Language`] for the given file.
pub fn get(&self, path: &Path) -> Option<Language> {
let ext = path.extension()?.to_str()?;
self.mapping.get(ext).copied()
}
}

View file

@ -117,6 +117,7 @@ pub struct Configuration {
pub output_format: Option<SerializationFormat>,
pub preview: Option<PreviewMode>,
pub required_version: Option<Version>,
pub extension: Option<ExtensionMapping>,
pub show_fixes: Option<bool>,
pub show_source: Option<bool>,
@ -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<Vec<FilePattern>>,
pub preview: Option<PreviewMode>,
pub extension: Option<ExtensionMapping>,
// Rule selection
pub extend_per_file_ignores: Vec<PerFileIgnore>,
@ -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<Vec<FilePattern>>,
pub preview: Option<PreviewMode>,
pub extension: Option<ExtensionMapping>,
pub indent_style: Option<IndentStyle>,
pub quote_style: Option<QuoteStyle>,
@ -1044,6 +1046,9 @@ impl FormatConfiguration {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: FormatOptions, project_root: &Path) -> Result<Self> {
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),
}
}
}

View file

@ -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(),

View file

@ -528,6 +528,8 @@ Options:
Enable preview mode; checks will include unstable rules and fixes. Use `--no-preview` to disable
--config <CONFIG>
Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
--extension <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 <CONFIG>
Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
--extension <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 <TARGET_VERSION>
The minimum Python version that should be supported [possible values: py37, py38, py39, py310, py311, py312]
--preview