Check pyproject.toml correctly when it is passed via stdin (#16971)

## Summary

Resolves #16950 and [a 1.5-year-old TODO
comment](8d16a5c8c9/crates/ruff/src/diagnostics.rs (L380)).

After this change, a `pyproject.toml` will be linted the same as any
Python files would when passed via stdin.

## Test Plan

Integration tests.
This commit is contained in:
InSync 2025-03-27 23:01:45 +07:00 committed by GitHub
parent b9a7328789
commit 6ef522159d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 220 additions and 5 deletions

View file

@ -367,8 +367,7 @@ pub(crate) fn lint_path(
})
}
/// Generate `Diagnostic`s from source code content derived from
/// stdin.
/// Generate `Diagnostic`s from source code content derived from stdin.
pub(crate) fn lint_stdin(
path: Option<&Path>,
package: Option<PackageRoot<'_>>,
@ -377,13 +376,37 @@ pub(crate) fn lint_stdin(
noqa: flags::Noqa,
fix_mode: flags::FixMode,
) -> Result<Diagnostics> {
// TODO(charlie): Support `pyproject.toml`.
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());
SourceType::Toml(source_type) if source_type.is_pyproject() => {
if !settings
.linter
.rules
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_pyproject_toml())
{
return Ok(Diagnostics::default());
}
let path = path.unwrap();
let source_file =
SourceFileBuilder::new(path.to_string_lossy(), contents.clone()).finish();
match fix_mode {
flags::FixMode::Diff | flags::FixMode::Generate => {}
flags::FixMode::Apply => write!(&mut io::stdout().lock(), "{contents}")?,
}
return Ok(Diagnostics {
messages: lint_pyproject_toml(source_file, &settings.linter),
fixed: FixMap::from_iter([(fs::relativize_path(path), FixTable::default())]),
notebook_indexes: FxHashMap::default(),
});
}
SourceType::Toml(_) => return Ok(Diagnostics::default()),
},
Some(language) => PySourceType::from(language),
};

View file

@ -2233,3 +2233,195 @@ unfixable = ["RUF"]
Ok(())
}
#[test]
fn pyproject_toml_stdin_syntax_error() {
let mut cmd = RuffCheck::default()
.args(["--stdin-filename", "pyproject.toml", "--select", "RUF200"])
.build();
assert_cmd_snapshot!(
cmd.pass_stdin("[project"),
@r"
success: false
exit_code: 1
----- stdout -----
pyproject.toml:1:9: RUF200 Failed to parse pyproject.toml: invalid table header
expected `.`, `]`
|
1 | [project
| ^ RUF200
|
Found 1 error.
----- stderr -----
"
);
}
#[test]
fn pyproject_toml_stdin_schema_error() {
let mut cmd = RuffCheck::default()
.args(["--stdin-filename", "pyproject.toml", "--select", "RUF200"])
.build();
assert_cmd_snapshot!(
cmd.pass_stdin("[project]\nname = 1"),
@r"
success: false
exit_code: 1
----- stdout -----
pyproject.toml:2:8: RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
|
1 | [project]
2 | name = 1
| ^ RUF200
|
Found 1 error.
----- stderr -----
"
);
}
#[test]
fn pyproject_toml_stdin_no_applicable_rules_selected() {
let mut cmd = RuffCheck::default()
.args(["--stdin-filename", "pyproject.toml"])
.build();
assert_cmd_snapshot!(
cmd.pass_stdin("[project"),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"
);
}
#[test]
fn pyproject_toml_stdin_no_applicable_rules_selected_2() {
let mut cmd = RuffCheck::default()
.args(["--stdin-filename", "pyproject.toml", "--select", "F401"])
.build();
assert_cmd_snapshot!(
cmd.pass_stdin("[project"),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"
);
}
#[test]
fn pyproject_toml_stdin_no_errors() {
let mut cmd = RuffCheck::default()
.args(["--stdin-filename", "pyproject.toml"])
.build();
assert_cmd_snapshot!(
cmd.pass_stdin(r#"[project]\nname = "ruff"\nversion = "0.0.0""#),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"
);
}
#[test]
fn pyproject_toml_stdin_schema_error_fix() {
let mut cmd = RuffCheck::default()
.args([
"--stdin-filename",
"pyproject.toml",
"--select",
"RUF200",
"--fix",
])
.build();
assert_cmd_snapshot!(
cmd.pass_stdin("[project]\nname = 1"),
@r"
success: false
exit_code: 1
----- stdout -----
[project]
name = 1
----- stderr -----
pyproject.toml:2:8: RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
|
1 | [project]
2 | name = 1
| ^ RUF200
|
Found 1 error.
"
);
}
#[test]
fn pyproject_toml_stdin_schema_error_fix_only() {
let mut cmd = RuffCheck::default()
.args([
"--stdin-filename",
"pyproject.toml",
"--select",
"RUF200",
"--fix-only",
])
.build();
assert_cmd_snapshot!(
cmd.pass_stdin("[project]\nname = 1"),
@r"
success: true
exit_code: 0
----- stdout -----
[project]
name = 1
----- stderr -----
"
);
}
#[test]
fn pyproject_toml_stdin_schema_error_fix_diff() {
let mut cmd = RuffCheck::default()
.args([
"--stdin-filename",
"pyproject.toml",
"--select",
"RUF200",
"--fix",
"--diff",
])
.build();
assert_cmd_snapshot!(
cmd.pass_stdin("[project]\nname = 1"),
@r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
"
);
}