Compare commits

...

18 Commits

Author SHA1 Message Date
Dhruv Manilawala
c0fc84fd78 Refactor boolean expression parsing 2024-04-21 11:53:15 +05:30
Dhruv Manilawala
060141b1de Make associativity a property of precedence 2024-04-20 23:55:20 +05:30
Dhruv Manilawala
b04cb9f92a Add ExpressionContext for expression parsing
This commit adds a new `ExpressionContext` struct which is used in
expression parsing.

This solves the following problem:
1. Allowing starred expression with different precedence
2. Allowing yield expression in certain context
3. Remove ambiguity with `in` keyword when parsing a `for ... in`
   statement
2024-04-20 15:25:13 +05:30
Carl Meyer
c80b9a4a90 Reduce size of Stmt from 144 to 120 bytes (#11051)
## Summary

I happened to notice that we box `TypeParams` on `StmtClassDef` but not
on `StmtFunctionDef` and wondered why, since `StmtFunctionDef` is bigger
and sets the size of `Stmt`.

@charliermarsh found that at the time we started boxing type params on
classes, classes were the largest statement type (see #6275), but that's
no longer true.

So boxing type-params also on functions reduces the overall size of
`Stmt`.

## Test Plan

The `<=` size tests are a bit irritating (since their failure doesn't
tell you the actual size), but I manually confirmed that the size is
actually 120 now.
2024-04-19 17:02:17 -06:00
Charlie Marsh
99f7f94538 Improve documentation around custom isort sections (#11050)
## Summary

Closes https://github.com/astral-sh/ruff/issues/11047.
2024-04-19 22:26:55 +00:00
James Frost
7b3c92a979 [flake8-bugbear] Document explicitly disabling strict zip (B905) (#11040)
Occasionally you intentionally have iterables of differing lengths. The
rule permits this by explicitly adding `strict=False`, but this was not
documented.

## Summary

The rule does not currently document how to avoid it when having
differing length iterables is intentional. This PR adds that to the rule
documentation.
2024-04-19 13:50:18 +00:00
Alex Waygood
fdbcb62adc scripts/fuzz-parser: work around race condition from running cargo build concurrently (#11039) 2024-04-19 14:42:28 +01:00
Dhruv Manilawala
0ff25a540c Bump version to 0.4.1 (#11035)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-04-19 17:42:02 +05:30
Alex Waygood
34873ec009 Add a script to fuzz the parser (courtesy of pysource-codegen) (#11015) 2024-04-19 12:40:36 +01:00
Dhruv Manilawala
d3cd61f804 Use empty range when there's "gap" in token source (#11032)
## Summary

This fixes a bug where the parser would panic when there is a "gap" in
the token source.

What's a gap?

The reason it's `<=` instead of just `==` is because there could be
whitespaces between
the two tokens. For example:

```python
#     last token end
#     | current token (newline) start
#     v v
def foo \n
#      ^
#      assume there's trailing whitespace here
```

Or, there could tokens that are considered "trivia" and thus aren't
emitted by the token
source. These are comments and non-logical newlines. For example:

```python
#     last token end
#     v
def foo # comment\n
#                ^ current token (newline) start
```

In either of the above cases, there's a "gap" between the end of the
last token and start
of the current token.

## Test Plan

Add test cases and update the snapshots.
2024-04-19 11:36:26 +00:00
Alex Waygood
9b80cc09ee Select fewer ruff rules when linting Python files in scripts/ (#11034) 2024-04-19 12:33:36 +01:00
Dhruv Manilawala
9bb23b0a38 Expect indented case block instead of match stmt (#11033)
## Summary

This PR adds a new `Clause::Case` and uses it to parse the body of a
`case` block. Earlier, it was using `Match` which would give an
incorrect error message like:

```
  |
1 | match subject:
2 |     case 1:
3 |     case 2: ...
  |     ^^^^ Syntax Error: Expected an indented block after `match` statement
  |
```

## Test Plan

Add test case and update the snapshot.
2024-04-19 16:46:15 +05:30
Charlie Marsh
06c248a126 [ruff] Ignore stub functions in unused-async (RUF029) (#11026)
## Summary

We should ignore methods that appear to be stubs, e.g.:

```python
async def foo() -> int: ...
```

Closes https://github.com/astral-sh/ruff/issues/11018.
2024-04-19 00:03:52 -04:00
Tibor Reiss
27902b7130 [pylint] Implement invalid-index-returned (PLE0305) (#10962)
Add pylint rule invalid-index-returned (PLE0305)

See https://github.com/astral-sh/ruff/issues/970 for rules

Test Plan: `cargo test`
2024-04-19 03:44:05 +00:00
Henry Asa
97acf1d59b ENH: Bump ruff dependency versions to support the latest release of v0.4.0 and Python 3.12 (#11025)
## Summary

With the release of
[`v0.4.0`](https://github.com/astral-sh/ruff/releases/tag/v0.4.0) of
`ruff`, I noticed that some of `ruff`'s dependencies were not updated to
their latest versions. The
[`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit)
package released
[`v0.4.0`](https://github.com/astral-sh/ruff-pre-commit/releases/tag/v0.4.0)
at the same time `ruff` was updated, but `ruff` still referenced
`v0.3.7` of the package, not the newly updated version. I updated the
`ruff-pre-commit` reference to be `v0.4.0`.

In a similar light, I noticed that the version of the
[`dill`](https://github.com/uqfoundation/dill) package being used was
not the latest version. I bumped `dill` from version `0.3.7` to `0.3.8`,
which now [fully supports Python
3.12](https://github.com/uqfoundation/dill/releases/tag/0.3.8).

## Related Issues

Resolves #11024
2024-04-19 03:37:54 +00:00
Tibor Reiss
adf63d9013 [pylint] Implement invalid-hash-returned (PLE0309) (#10961)
Add pylint rule invalid-hash-returned (PLE0309)

See https://github.com/astral-sh/ruff/issues/970 for rules

Test Plan: `cargo test`

TBD: from the description: "Strictly speaking `bool` is a subclass of
`int`, thus returning `True`/`False` is valid. To be consistent with
other rules (e.g.
[PLE0305](https://github.com/astral-sh/ruff/pull/10962)
invalid-index-returned), ruff will raise, compared to pylint which will
not raise."
2024-04-19 03:33:52 +00:00
MithicSpirit
5d3c9f2637 ruff server: fix Neovim setup guide command (#11021) 2024-04-19 08:24:09 +05:30
Charlie Marsh
33529c049e Allow NoReturn-like functions for __str__, __len__, etc. (#11017)
## Summary

If the method always raises, we shouldn't raise a diagnostic for
"returning a value of the wrong type".

Closes https://github.com/astral-sh/ruff/issues/11016.
2024-04-18 22:55:15 +00:00
73 changed files with 2592 additions and 1166 deletions

View File

@@ -55,7 +55,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.7
rev: v0.4.0
hooks:
- id: ruff-format
- id: ruff

View File

@@ -1,5 +1,19 @@
# Changelog
## 0.4.1
### Preview features
- \[`pylint`\] Implement `invalid-hash-returned` (`PLE0309`) ([#10961](https://github.com/astral-sh/ruff/pull/10961))
- \[`pylint`\] Implement `invalid-index-returned` (`PLE0305`) ([#10962](https://github.com/astral-sh/ruff/pull/10962))
### Bug fixes
- \[`pylint`\] Allow `NoReturn`-like functions for `__str__`, `__len__`, etc. (`PLE0307`) ([#11017](https://github.com/astral-sh/ruff/pull/11017))
- Parser: Use empty range when there's "gap" in token source ([#11032](https://github.com/astral-sh/ruff/pull/11032))
- \[`ruff`\] Ignore stub functions in `unused-async` (`RUF029`) ([#11026](https://github.com/astral-sh/ruff/pull/11026))
- Parser: Expect indented case block instead of match stmt ([#11033](https://github.com/astral-sh/ruff/pull/11033))
## 0.4.0
### A new, hand-written parser

7
Cargo.lock generated
View File

@@ -1835,7 +1835,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.4.0"
version = "0.4.1"
dependencies = [
"anyhow",
"argfile",
@@ -1997,7 +1997,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.4.0"
version = "0.4.1"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2180,7 +2180,6 @@ dependencies = [
"anyhow",
"bitflags 2.5.0",
"bstr",
"drop_bomb",
"insta",
"is-macro",
"itertools 0.12.1",
@@ -2272,7 +2271,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.4.0"
version = "0.4.1"
dependencies = [
"anyhow",
"clap",

View File

@@ -151,7 +151,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.0
rev: v0.4.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.4.0"
version = "0.4.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.4.0"
version = "0.4.1"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -21,12 +21,6 @@ class BytesNoReturn:
print("ruff") # [invalid-bytes-return]
class BytesWrongRaise:
def __bytes__(self):
print("raise some error")
raise NotImplementedError # [invalid-bytes-return]
# TODO: Once Ruff has better type checking
def return_bytes():
return "some string"
@@ -63,3 +57,9 @@ class Bytes4:
class Bytes5:
def __bytes__(self):
raise NotImplementedError
class Bytes6:
def __bytes__(self):
print("raise some error")
raise NotImplementedError

View File

@@ -0,0 +1,65 @@
# These testcases should raise errors
class Bool:
def __hash__(self):
return True # [invalid-hash-return]
class Float:
def __hash__(self):
return 3.05 # [invalid-hash-return]
class Str:
def __hash__(self):
return "ruff" # [invalid-hash-return]
class HashNoReturn:
def __hash__(self):
print("ruff") # [invalid-hash-return]
# TODO: Once Ruff has better type checking
def return_int():
return "3"
class ComplexReturn:
def __hash__(self):
return return_int() # [invalid-hash-return]
# These testcases should NOT raise errors
class Hash:
def __hash__(self):
return 7741
class Hash2:
def __hash__(self):
x = 7741
return x
class Hash3:
def __hash__(self): ...
class Has4:
def __hash__(self):
pass
class Hash5:
def __hash__(self):
raise NotImplementedError
class HashWrong6:
def __hash__(self):
print("raise some error")
raise NotImplementedError

View File

@@ -0,0 +1,73 @@
# These testcases should raise errors
class Bool:
"""pylint would not raise, but ruff does - see explanation in the docs"""
def __index__(self):
return True # [invalid-index-return]
class Float:
def __index__(self):
return 3.05 # [invalid-index-return]
class Dict:
def __index__(self):
return {"1": "1"} # [invalid-index-return]
class Str:
def __index__(self):
return "ruff" # [invalid-index-return]
class IndexNoReturn:
def __index__(self):
print("ruff") # [invalid-index-return]
# TODO: Once Ruff has better type checking
def return_index():
return "3"
class ComplexReturn:
def __index__(self):
return return_index() # [invalid-index-return]
# These testcases should NOT raise errors
class Index:
def __index__(self):
return 0
class Index2:
def __index__(self):
x = 1
return x
class Index3:
def __index__(self):
...
class Index4:
def __index__(self):
pass
class Index5:
def __index__(self):
raise NotImplementedError
class Index6:
def __index__(self):
print("raise some error")
raise NotImplementedError

View File

@@ -26,12 +26,6 @@ class LengthNegative:
return -42 # [invalid-length-return]
class LengthWrongRaise:
def __len__(self):
print("raise some error")
raise NotImplementedError # [invalid-length-return]
# TODO: Once Ruff has better type checking
def return_int():
return "3"
@@ -68,3 +62,9 @@ class Length4:
class Length5:
def __len__(self):
raise NotImplementedError
class Length6:
def __len__(self):
print("raise some error")
raise NotImplementedError

View File

@@ -47,3 +47,14 @@ class Str2:
class Str3:
def __str__(self): ...
class Str4:
def __str__(self):
raise RuntimeError("__str__ not allowed")
class Str5:
def __str__(self): # PLE0307 (returns None if x <= 0)
if x > 0:
raise RuntimeError("__str__ not allowed")

View File

@@ -22,7 +22,7 @@ async def pass_3(): # OK: uses an async loop
class Foo:
async def pass_4(): # OK: method of a class
async def pass_4(self): # OK: method of a class
pass
@@ -31,6 +31,10 @@ def foo():
await bla
async def pass_6(): # OK: just a stub
...
async def fail_1a(): # RUF029
time.sleep(1)
@@ -58,7 +62,7 @@ async def fail_4a(): # RUF029: the /outer/ function does not await
async def fail_4b(): # RUF029: the /outer/ function does not await
class Foo:
async def foo():
async def foo(self):
await bla

View File

@@ -103,6 +103,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::InvalidBytesReturnType) {
pylint::rules::invalid_bytes_return(checker, function_def);
}
if checker.enabled(Rule::InvalidIndexReturnType) {
pylint::rules::invalid_index_return(checker, function_def);
}
if checker.enabled(Rule::InvalidHashReturnType) {
pylint::rules::invalid_hash_return(checker, function_def);
}
if checker.enabled(Rule::InvalidStrReturnType) {
pylint::rules::invalid_str_return(checker, function_def);
}
@@ -152,7 +158,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
decorator_list,
returns.as_ref().map(AsRef::as_ref),
parameters,
type_params.as_ref(),
type_params.as_deref(),
);
}
if checker.source_type.is_stub() {

View File

@@ -243,8 +243,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E0302") => (RuleGroup::Stable, rules::pylint::rules::UnexpectedSpecialMethodSignature),
(Pylint, "E0303") => (RuleGroup::Preview, rules::pylint::rules::InvalidLengthReturnType),
(Pylint, "E0304") => (RuleGroup::Preview, rules::pylint::rules::InvalidBoolReturnType),
(Pylint, "E0305") => (RuleGroup::Preview, rules::pylint::rules::InvalidIndexReturnType),
(Pylint, "E0307") => (RuleGroup::Stable, rules::pylint::rules::InvalidStrReturnType),
(Pylint, "E0308") => (RuleGroup::Preview, rules::pylint::rules::InvalidBytesReturnType),
(Pylint, "E0309") => (RuleGroup::Preview, rules::pylint::rules::InvalidHashReturnType),
(Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject),
(Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat),
(Pylint, "E0643") => (RuleGroup::Preview, rules::pylint::rules::PotentialIndexError),

View File

@@ -17,7 +17,8 @@ use crate::fix::edits::add_argument;
/// iterable. This can lead to subtle bugs.
///
/// Use the `strict` parameter to raise a `ValueError` if the iterables are of
/// non-uniform length.
/// non-uniform length. If the iterables are intentionally different lengths, the
/// parameter should be explicitly set to `False`.
///
/// ## Example
/// ```python

View File

@@ -77,14 +77,19 @@ mod tests {
#[test_case(Rule::InvalidAllFormat, Path::new("invalid_all_format.py"))]
#[test_case(Rule::InvalidAllObject, Path::new("invalid_all_object.py"))]
#[test_case(Rule::InvalidBoolReturnType, Path::new("invalid_return_type_bool.py"))]
#[test_case(
Rule::InvalidLengthReturnType,
Path::new("invalid_return_type_length.py")
)]
#[test_case(
Rule::InvalidBytesReturnType,
Path::new("invalid_return_type_bytes.py")
)]
#[test_case(
Rule::InvalidIndexReturnType,
Path::new("invalid_return_type_index.py")
)]
#[test_case(Rule::InvalidHashReturnType, Path::new("invalid_return_type_hash.py"))]
#[test_case(
Rule::InvalidLengthReturnType,
Path::new("invalid_return_type_length.py")
)]
#[test_case(Rule::InvalidStrReturnType, Path::new("invalid_return_type_str.py"))]
#[test_case(Rule::DuplicateBases, Path::new("duplicate_bases.py"))]
#[test_case(Rule::InvalidCharacterBackspace, Path::new("invalid_characters.py"))]

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast};
use ruff_python_semantic::analyze::function_type::is_stub;
use ruff_python_semantic::analyze::terminal::Terminal;
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_text_size::Ranged;
@@ -43,7 +44,7 @@ impl Violation for InvalidBoolReturnType {
}
}
/// E0307
/// PLE0304
pub(crate) fn invalid_bool_return(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
if function_def.name.as_str() != "__bool__" {
return;
@@ -57,19 +58,29 @@ pub(crate) fn invalid_bool_return(checker: &mut Checker, function_def: &ast::Stm
return;
}
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
let terminal = Terminal::from_function(function_def);
// If every control flow path raises an exception, ignore the function.
if terminal == Terminal::Raise {
return;
}
// If there are no return statements, add a diagnostic.
if terminal == Terminal::Implicit {
checker.diagnostics.push(Diagnostic::new(
InvalidBoolReturnType,
function_def.identifier(),
));
return;
}
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function_def.body);
visitor.returns
};
if returns.is_empty() {
checker.diagnostics.push(Diagnostic::new(
InvalidBoolReturnType,
function_def.identifier(),
));
}
for stmt in returns {
if let Some(value) = stmt.value.as_deref() {
if !matches!(

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast};
use ruff_python_semantic::analyze::function_type::is_stub;
use ruff_python_semantic::analyze::terminal::Terminal;
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType};
use ruff_text_size::Ranged;
@@ -43,7 +44,7 @@ impl Violation for InvalidBytesReturnType {
}
}
/// E0308
/// PLE0308
pub(crate) fn invalid_bytes_return(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
if function_def.name.as_str() != "__bytes__" {
return;
@@ -57,19 +58,29 @@ pub(crate) fn invalid_bytes_return(checker: &mut Checker, function_def: &ast::St
return;
}
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
let terminal = Terminal::from_function(function_def);
// If every control flow path raises an exception, ignore the function.
if terminal == Terminal::Raise {
return;
}
// If there are no return statements, add a diagnostic.
if terminal == Terminal::Implicit {
checker.diagnostics.push(Diagnostic::new(
InvalidBytesReturnType,
function_def.identifier(),
));
return;
}
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function_def.body);
visitor.returns
};
if returns.is_empty() {
checker.diagnostics.push(Diagnostic::new(
InvalidBytesReturnType,
function_def.identifier(),
));
}
for stmt in returns {
if let Some(value) = stmt.value.as_deref() {
if !matches!(

View File

@@ -0,0 +1,107 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast};
use ruff_python_semantic::analyze::function_type::is_stub;
use ruff_python_semantic::analyze::terminal::Terminal;
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `__hash__` implementations that return a type other than `integer`.
///
/// ## Why is this bad?
/// The `__hash__` method should return an `integer`. Returning a different
/// type may cause unexpected behavior.
///
/// Note: `bool` is a subclass of `int`, so it's technically valid for `__hash__` to
/// return `True` or `False`. However, for consistency with other rules, Ruff will
/// still raise when `__hash__` returns a `bool`.
///
/// ## Example
/// ```python
/// class Foo:
/// def __hash__(self):
/// return "2"
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// def __hash__(self):
/// return 2
/// ```
///
///
/// ## References
/// - [Python documentation: The `__hash__` method](https://docs.python.org/3/reference/datamodel.html#object.__hash__)
#[violation]
pub struct InvalidHashReturnType;
impl Violation for InvalidHashReturnType {
#[derive_message_formats]
fn message(&self) -> String {
format!("`__hash__` does not return an integer")
}
}
/// E0309
pub(crate) fn invalid_hash_return(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
if function_def.name.as_str() != "__hash__" {
return;
}
if !checker.semantic().current_scope().kind.is_class() {
return;
}
if is_stub(function_def, checker.semantic()) {
return;
}
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
let terminal = Terminal::from_function(function_def);
// If every control flow path raises an exception, ignore the function.
if terminal == Terminal::Raise {
return;
}
// If there are no return statements, add a diagnostic.
if terminal == Terminal::Implicit {
checker.diagnostics.push(Diagnostic::new(
InvalidHashReturnType,
function_def.identifier(),
));
return;
}
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function_def.body);
visitor.returns
};
for stmt in returns {
if let Some(value) = stmt.value.as_deref() {
if !matches!(
ResolvedPythonType::from(value),
ResolvedPythonType::Unknown
| ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer))
) {
checker
.diagnostics
.push(Diagnostic::new(InvalidHashReturnType, value.range()));
}
} else {
// Disallow implicit `None`.
checker
.diagnostics
.push(Diagnostic::new(InvalidHashReturnType, stmt.range()));
}
}
}

View File

@@ -0,0 +1,109 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast};
use ruff_python_semantic::analyze::function_type::is_stub;
use ruff_python_semantic::analyze::terminal::Terminal;
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `__index__` implementations that return a type other than `integer`.
///
/// ## Why is this bad?
/// The `__index__` method should return an `integer`. Returning a different
/// type may cause unexpected behavior.
///
/// Note: `bool` is a subclass of `int`, so it's technically valid for `__index__` to
/// return `True` or `False`. However, a DeprecationWarning (`DeprecationWarning:
/// __index__ returned non-int (type bool)`) for such cases was already introduced,
/// thus this is a conscious difference between the original pylint rule and the
/// current ruff implementation.
///
/// ## Example
/// ```python
/// class Foo:
/// def __index__(self):
/// return "2"
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// def __index__(self):
/// return 2
/// ```
///
///
/// ## References
/// - [Python documentation: The `__index__` method](https://docs.python.org/3/reference/datamodel.html#object.__index__)
#[violation]
pub struct InvalidIndexReturnType;
impl Violation for InvalidIndexReturnType {
#[derive_message_formats]
fn message(&self) -> String {
format!("`__index__` does not return an integer")
}
}
/// E0305
pub(crate) fn invalid_index_return(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
if function_def.name.as_str() != "__index__" {
return;
}
if !checker.semantic().current_scope().kind.is_class() {
return;
}
if is_stub(function_def, checker.semantic()) {
return;
}
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
let terminal = Terminal::from_function(function_def);
// If every control flow path raises an exception, ignore the function.
if terminal == Terminal::Raise {
return;
}
// If there are no return statements, add a diagnostic.
if terminal == Terminal::Implicit {
checker.diagnostics.push(Diagnostic::new(
InvalidIndexReturnType,
function_def.identifier(),
));
return;
}
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function_def.body);
visitor.returns
};
for stmt in returns {
if let Some(value) = stmt.value.as_deref() {
if !matches!(
ResolvedPythonType::from(value),
ResolvedPythonType::Unknown
| ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer))
) {
checker
.diagnostics
.push(Diagnostic::new(InvalidIndexReturnType, value.range()));
}
} else {
// Disallow implicit `None`.
checker
.diagnostics
.push(Diagnostic::new(InvalidIndexReturnType, stmt.range()));
}
}
}

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::function_type::is_stub;
use ruff_python_semantic::analyze::terminal::Terminal;
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
use ruff_text_size::Ranged;
@@ -63,19 +64,29 @@ pub(crate) fn invalid_length_return(checker: &mut Checker, function_def: &ast::S
return;
}
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
let terminal = Terminal::from_function(function_def);
// If every control flow path raises an exception, ignore the function.
if terminal == Terminal::Raise {
return;
}
// If there are no return statements, add a diagnostic.
if terminal == Terminal::Implicit {
checker.diagnostics.push(Diagnostic::new(
InvalidLengthReturnType,
function_def.identifier(),
));
return;
}
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function_def.body);
visitor.returns
};
if returns.is_empty() {
checker.diagnostics.push(Diagnostic::new(
InvalidLengthReturnType,
function_def.identifier(),
));
}
for stmt in returns {
if let Some(value) = stmt.value.as_deref() {
if is_negative_integer(value)

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast};
use ruff_python_semantic::analyze::function_type::is_stub;
use ruff_python_semantic::analyze::terminal::Terminal;
use ruff_python_semantic::analyze::type_inference::{PythonType, ResolvedPythonType};
use ruff_text_size::Ranged;
@@ -57,19 +58,29 @@ pub(crate) fn invalid_str_return(checker: &mut Checker, function_def: &ast::Stmt
return;
}
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
let terminal = Terminal::from_function(function_def);
// If every control flow path raises an exception, ignore the function.
if terminal == Terminal::Raise {
return;
}
// If there are no return statements, add a diagnostic.
if terminal == Terminal::Implicit {
checker.diagnostics.push(Diagnostic::new(
InvalidStrReturnType,
function_def.identifier(),
));
return;
}
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function_def.body);
visitor.returns
};
if returns.is_empty() {
checker.diagnostics.push(Diagnostic::new(
InvalidStrReturnType,
function_def.identifier(),
));
}
for stmt in returns {
if let Some(value) = stmt.value.as_deref() {
if !matches!(

View File

@@ -31,6 +31,8 @@ pub(crate) use invalid_bool_return::*;
pub(crate) use invalid_bytes_return::*;
pub(crate) use invalid_envvar_default::*;
pub(crate) use invalid_envvar_value::*;
pub(crate) use invalid_hash_return::*;
pub(crate) use invalid_index_return::*;
pub(crate) use invalid_length_return::*;
pub(crate) use invalid_str_return::*;
pub(crate) use invalid_string_characters::*;
@@ -131,6 +133,8 @@ mod invalid_bool_return;
mod invalid_bytes_return;
mod invalid_envvar_default;
mod invalid_envvar_value;
mod invalid_hash_return;
mod invalid_index_return;
mod invalid_length_return;
mod invalid_str_return;
mod invalid_string_characters;

View File

@@ -59,7 +59,7 @@ impl Violation for NonSlotAssignment {
}
}
/// E0237
/// PLE0237
pub(crate) fn non_slot_assignment(checker: &mut Checker, class_def: &ast::StmtClassDef) {
let semantic = checker.semantic();

View File

@@ -40,12 +40,3 @@ invalid_return_type_length.py:26:16: PLE0303 `__len__` does not return a non-neg
26 | return -42 # [invalid-length-return]
| ^^^ PLE0303
|
invalid_return_type_length.py:30:9: PLE0303 `__len__` does not return a non-negative integer
|
29 | class LengthWrongRaise:
30 | def __len__(self):
| ^^^^^^^ PLE0303
31 | print("raise some error")
32 | raise NotImplementedError # [invalid-length-return]
|

View File

@@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
invalid_return_type_index.py:8:16: PLE0305 `__index__` does not return an integer
|
7 | def __index__(self):
8 | return True # [invalid-index-return]
| ^^^^ PLE0305
|
invalid_return_type_index.py:13:16: PLE0305 `__index__` does not return an integer
|
11 | class Float:
12 | def __index__(self):
13 | return 3.05 # [invalid-index-return]
| ^^^^ PLE0305
|
invalid_return_type_index.py:18:16: PLE0305 `__index__` does not return an integer
|
16 | class Dict:
17 | def __index__(self):
18 | return {"1": "1"} # [invalid-index-return]
| ^^^^^^^^^^ PLE0305
|
invalid_return_type_index.py:23:16: PLE0305 `__index__` does not return an integer
|
21 | class Str:
22 | def __index__(self):
23 | return "ruff" # [invalid-index-return]
| ^^^^^^ PLE0305
|
invalid_return_type_index.py:27:9: PLE0305 `__index__` does not return an integer
|
26 | class IndexNoReturn:
27 | def __index__(self):
| ^^^^^^^^^ PLE0305
28 | print("ruff") # [invalid-index-return]
|

View File

@@ -32,3 +32,12 @@ invalid_return_type_str.py:21:16: PLE0307 `__str__` does not return `str`
21 | return False
| ^^^^^ PLE0307
|
invalid_return_type_str.py:58:9: PLE0307 `__str__` does not return `str`
|
57 | class Str5:
58 | def __str__(self): # PLE0307 (returns None if x <= 0)
| ^^^^^^^ PLE0307
59 | if x > 0:
60 | raise RuntimeError("__str__ not allowed")
|

View File

@@ -32,12 +32,3 @@ invalid_return_type_bytes.py:20:9: PLE0308 `__bytes__` does not return `bytes`
| ^^^^^^^^^ PLE0308
21 | print("ruff") # [invalid-bytes-return]
|
invalid_return_type_bytes.py:25:9: PLE0308 `__bytes__` does not return `bytes`
|
24 | class BytesWrongRaise:
25 | def __bytes__(self):
| ^^^^^^^^^ PLE0308
26 | print("raise some error")
27 | raise NotImplementedError # [invalid-bytes-return]
|

View File

@@ -0,0 +1,34 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
invalid_return_type_hash.py:6:16: PLE0309 `__hash__` does not return an integer
|
4 | class Bool:
5 | def __hash__(self):
6 | return True # [invalid-hash-return]
| ^^^^ PLE0309
|
invalid_return_type_hash.py:11:16: PLE0309 `__hash__` does not return an integer
|
9 | class Float:
10 | def __hash__(self):
11 | return 3.05 # [invalid-hash-return]
| ^^^^ PLE0309
|
invalid_return_type_hash.py:16:16: PLE0309 `__hash__` does not return an integer
|
14 | class Str:
15 | def __hash__(self):
16 | return "ruff" # [invalid-hash-return]
| ^^^^^^ PLE0309
|
invalid_return_type_hash.py:20:9: PLE0309 `__hash__` does not return an integer
|
19 | class HashNoReturn:
20 | def __hash__(self):
| ^^^^^^^^ PLE0309
21 | print("ruff") # [invalid-hash-return]
|

View File

@@ -3,6 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::visitor::preorder;
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, Stmt};
use ruff_python_semantic::analyze::function_type::is_stub;
use crate::checkers::ast::Checker;
@@ -160,6 +161,11 @@ pub(crate) fn unused_async(
return;
}
// Ignore stubs (e.g., `...`).
if is_stub(function_def, checker.semantic()) {
return;
}
let found_await_or_async = {
let mut visitor = AsyncExprVisitor::default();
preorder::walk_body(&mut visitor, body);

View File

@@ -1,56 +1,48 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
---
RUF029.py:34:11: RUF029 Function `fail_1a` is declared `async`, but doesn't `await` or use `async` features.
RUF029.py:38:11: RUF029 Function `fail_1a` is declared `async`, but doesn't `await` or use `async` features.
|
34 | async def fail_1a(): # RUF029
38 | async def fail_1a(): # RUF029
| ^^^^^^^ RUF029
35 | time.sleep(1)
39 | time.sleep(1)
|
RUF029.py:38:11: RUF029 Function `fail_1b` is declared `async`, but doesn't `await` or use `async` features.
RUF029.py:42:11: RUF029 Function `fail_1b` is declared `async`, but doesn't `await` or use `async` features.
|
38 | async def fail_1b(): # RUF029: yield does not require async
42 | async def fail_1b(): # RUF029: yield does not require async
| ^^^^^^^ RUF029
39 | yield "hello"
43 | yield "hello"
|
RUF029.py:42:11: RUF029 Function `fail_2` is declared `async`, but doesn't `await` or use `async` features.
RUF029.py:46:11: RUF029 Function `fail_2` is declared `async`, but doesn't `await` or use `async` features.
|
42 | async def fail_2(): # RUF029
46 | async def fail_2(): # RUF029
| ^^^^^^ RUF029
43 | with None as i:
44 | pass
47 | with None as i:
48 | pass
|
RUF029.py:47:11: RUF029 Function `fail_3` is declared `async`, but doesn't `await` or use `async` features.
RUF029.py:51:11: RUF029 Function `fail_3` is declared `async`, but doesn't `await` or use `async` features.
|
47 | async def fail_3(): # RUF029
51 | async def fail_3(): # RUF029
| ^^^^^^ RUF029
48 | for i in []:
49 | pass
52 | for i in []:
53 | pass
|
RUF029.py:54:11: RUF029 Function `fail_4a` is declared `async`, but doesn't `await` or use `async` features.
RUF029.py:58:11: RUF029 Function `fail_4a` is declared `async`, but doesn't `await` or use `async` features.
|
54 | async def fail_4a(): # RUF029: the /outer/ function does not await
58 | async def fail_4a(): # RUF029: the /outer/ function does not await
| ^^^^^^^ RUF029
55 | async def foo():
56 | await bla
59 | async def foo():
60 | await bla
|
RUF029.py:59:11: RUF029 Function `fail_4b` is declared `async`, but doesn't `await` or use `async` features.
RUF029.py:63:11: RUF029 Function `fail_4b` is declared `async`, but doesn't `await` or use `async` features.
|
59 | async def fail_4b(): # RUF029: the /outer/ function does not await
63 | async def fail_4b(): # RUF029: the /outer/ function does not await
| ^^^^^^^ RUF029
60 | class Foo:
61 | async def foo():
|
RUF029.py:66:15: RUF029 Function `fail_4c` is declared `async`, but doesn't `await` or use `async` features.
|
65 | def foo():
66 | async def fail_4c(): # RUF029: the /inner/ function does not await
| ^^^^^^^ RUF029
67 | pass
64 | class Foo:
65 | async def foo(self):
|

View File

@@ -181,7 +181,7 @@ pub struct StmtFunctionDef {
pub is_async: bool,
pub decorator_list: Vec<Decorator>,
pub name: Identifier,
pub type_params: Option<TypeParams>,
pub type_params: Option<Box<TypeParams>>,
pub parameters: Box<Parameters>,
pub returns: Option<Box<Expr>>,
pub body: Vec<Stmt>,
@@ -4171,8 +4171,8 @@ mod tests {
#[test]
#[cfg(target_pointer_width = "64")]
fn size() {
assert!(std::mem::size_of::<Stmt>() <= 144);
assert!(std::mem::size_of::<StmtFunctionDef>() <= 144);
assert!(std::mem::size_of::<Stmt>() <= 120);
assert!(std::mem::size_of::<StmtFunctionDef>() <= 120);
assert!(std::mem::size_of::<StmtClassDef>() <= 104);
assert!(std::mem::size_of::<StmtTry>() <= 112);
assert!(std::mem::size_of::<Mod>() <= 32);

View File

@@ -108,7 +108,7 @@ impl<'a> ClauseHeader<'a> {
returns,
body: _,
}) => {
if let Some(type_params) = type_params.as_ref() {
if let Some(type_params) = type_params.as_deref() {
visit(type_params, visitor);
}
visit(parameters.as_ref(), visitor);

View File

@@ -18,7 +18,6 @@ ruff_text_size = { path = "../ruff_text_size" }
anyhow = { workspace = true }
bitflags = { workspace = true }
drop_bomb = { workspace = true }
bstr = { workspace = true }
is-macro = { workspace = true }
itertools = { workspace = true }

View File

@@ -0,0 +1,3 @@
match subject:
case 1:
case 2: ...

View File

@@ -1 +0,0 @@
for d(x in y) in target: ...

View File

@@ -3,4 +3,5 @@ for "a" in x: ...
for *x and y in z: ...
for *x | y in z: ...
for await x in z: ...
for yield x in y: ...
for [x, 1, y, *["a"]] in z: ...

View File

@@ -0,0 +1,6 @@
for x not in y in z: ...
for x == y in z: ...
for x or y in z: ...
for -x in y: ...
for not x in y: ...
for x | y in z: ...

View File

@@ -1,3 +1,4 @@
for d(x in y) in target: ...
for (x in y)() in iter: ...
for (x in y) in iter: ...
for (x in y, z) in iter: ...

View File

@@ -0,0 +1,3 @@
def foo # comment
def bar(): ...
def baz

View File

@@ -1 +0,0 @@
for d[x in y] in target: ...

View File

@@ -1,2 +1,3 @@
for d[x in y] in target: ...
for (x in y)[0] in iter: ...
for (x in y).attr in iter: ...

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
use std::cmp::Ordering;
use bitflags::bitflags;
use drop_bomb::DebugDropBomb;
use ast::Mod;
use ruff_python_ast as ast;
@@ -16,7 +15,7 @@ use crate::{
Mode, ParseError, ParseErrorType, Tok, TokenKind,
};
use self::expression::AllowStarredExpression;
use self::expression::ExpressionContext;
mod expression;
mod helpers;
@@ -77,13 +76,6 @@ pub(crate) struct Parser<'src> {
/// Stores all the syntax errors found during the parsing.
errors: Vec<ParseError>,
/// This tracks the current expression or statement being parsed.
///
/// The `ctx` is also used to create custom error messages and forbid certain
/// expressions or statements of being parsed. The `ctx` should be empty after
/// an expression or statement is done parsing.
ctx: ParserCtxFlags,
/// Specify the mode in which the code will be parsed.
mode: Mode,
@@ -123,7 +115,6 @@ impl<'src> Parser<'src> {
mode,
source,
errors: Vec::new(),
ctx: ParserCtxFlags::empty(),
tokens,
recovery_context: RecoveryContext::empty(),
last_token_end: tokens_range.start(),
@@ -136,7 +127,7 @@ impl<'src> Parser<'src> {
pub(crate) fn parse_program(mut self) -> Program {
let ast = if self.mode == Mode::Expression {
let start = self.node_start();
let parsed_expr = self.parse_expression_list(AllowStarredExpression::No);
let parsed_expr = self.parse_expression_list(ExpressionContext::default());
// All of the remaining newlines are actually going to be non-logical newlines.
self.eat(TokenKind::Newline);
@@ -185,9 +176,6 @@ impl<'src> Parser<'src> {
}
fn finish(self) -> Vec<ParseError> {
// After parsing, the `ctx` and `ctx_stack` should be empty.
// If it's not, you probably forgot to call `clear_ctx` somewhere.
assert_eq!(self.ctx, ParserCtxFlags::empty());
assert_eq!(
self.current_token_kind(),
TokenKind::EndOfFile,
@@ -232,41 +220,65 @@ impl<'src> Parser<'src> {
merged
}
#[inline]
#[must_use]
fn set_ctx(&mut self, ctx: ParserCtxFlags) -> SavedParserContext {
SavedParserContext {
flags: std::mem::replace(&mut self.ctx, ctx),
bomb: DebugDropBomb::new(
"You must restore the old parser context explicit by calling `restore_ctx`",
),
}
}
#[inline]
fn restore_ctx(&mut self, current: ParserCtxFlags, mut saved_context: SavedParserContext) {
assert_eq!(self.ctx, current);
saved_context.bomb.defuse();
self.ctx = saved_context.flags;
}
#[inline]
fn has_ctx(&self, ctx: ParserCtxFlags) -> bool {
self.ctx.intersects(ctx)
}
/// Returns the start position for a node that starts at the current token.
fn node_start(&self) -> TextSize {
self.current_token_range().start()
}
fn node_range(&self, start: TextSize) -> TextRange {
// It's possible during error recovery that the parsing didn't consume any tokens. In that case,
// `last_token_end` still points to the end of the previous token but `start` is the start of the current token.
// Calling `TextRange::new(start, self.last_token_end)` would panic in that case because `start > end`.
// This path "detects" this case and creates an empty range instead.
if self.node_start() == start {
TextRange::empty(start)
// It's possible during error recovery that the parsing didn't consume any tokens. In that
// case, `last_token_end` still points to the end of the previous token but `start` is the
// start of the current token. Calling `TextRange::new(start, self.last_token_end)` would
// panic in that case because `start > end`. This path "detects" this case and creates an
// empty range instead.
//
// The reason it's `<=` instead of just `==` is because there could be whitespaces between
// the two tokens. For example:
//
// ```python
// # last token end
// # | current token (newline) start
// # v v
// def foo \n
// # ^
// # assume there's trailing whitespace here
// ```
//
// Or, there could tokens that are considered "trivia" and thus aren't emitted by the token
// source. These are comments and non-logical newlines. For example:
//
// ```python
// # last token end
// # v
// def foo # comment\n
// # ^ current token (newline) start
// ```
//
// In either of the above cases, there's a "gap" between the end of the last token and start
// of the current token.
if self.last_token_end <= start {
// We need to create an empty range at the last token end instead of the start because
// otherwise this node range will fall outside the range of it's parent node. Taking
// the above example:
//
// ```python
// if True:
// # function start
// # | function end
// # v v
// def foo # comment
// # ^ current token start
// ```
//
// Here, the current token start is the start of parameter range but the function ends
// at `foo`. Even if there's a function body, the range of parameters would still be
// before the comment.
// test_err node_range_with_gaps
// def foo # comment
// def bar(): ...
// def baz
TextRange::empty(self.last_token_end)
} else {
TextRange::new(start, self.last_token_end)
}
@@ -628,13 +640,6 @@ impl SequenceMatchPatternParentheses {
}
}
bitflags! {
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
struct ParserCtxFlags: u8 {
const FOR_TARGET = 1 << 2;
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
enum FunctionKind {
/// A lambda expression, e.g., `lambda x: x`
@@ -1280,9 +1285,3 @@ impl RecoveryContext {
})
}
}
#[derive(Debug)]
struct SavedParserContext {
flags: ParserCtxFlags,
bomb: DebugDropBomb,
}

View File

@@ -6,6 +6,8 @@ use crate::parser::{recovery, Parser, RecoveryContextKind, SequenceMatchPatternP
use crate::token_set::TokenSet;
use crate::{ParseErrorType, Tok, TokenKind};
use super::expression::ExpressionContext;
/// The set of tokens that can start a literal pattern.
const LITERAL_PATTERN_START_SET: TokenSet = TokenSet::new([
TokenKind::None,
@@ -483,7 +485,7 @@ impl<'src> Parser<'src> {
TokenKind::Int | TokenKind::Float | TokenKind::Complex
) =>
{
let unary_expr = self.parse_unary_expression();
let unary_expr = self.parse_unary_expression(ExpressionContext::default());
if unary_expr.op.is_u_add() {
self.add_error(

View File

@@ -11,13 +11,12 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::parser::expression::{GeneratorExpressionInParentheses, ParsedExpr, EXPR_SET};
use crate::parser::progress::ParserProgress;
use crate::parser::{
helpers, FunctionKind, Parser, ParserCtxFlags, RecoveryContext, RecoveryContextKind,
WithItemKind,
helpers, FunctionKind, Parser, RecoveryContext, RecoveryContextKind, WithItemKind,
};
use crate::token_set::TokenSet;
use crate::{Mode, ParseErrorType, Tok, TokenKind};
use super::expression::{AllowNamedExpression, AllowStarredExpression, Precedence};
use super::expression::{ExpressionContext, OperatorPrecedence, StarredExpressionPrecedence};
use super::Parenthesized;
/// Tokens that represent compound statements.
@@ -261,8 +260,11 @@ impl<'src> Parser<'src> {
let start = self.node_start();
// simple_stmt: `... | yield_stmt | star_expressions | ...`
let parsed_expr =
self.parse_yield_expression_or_else(Parser::parse_star_expression_list);
let parsed_expr = self.parse_expression_list(
ExpressionContext::default()
.with_yield_expression_allowed()
.with_starred_expression_allowed(StarredExpressionPrecedence::BitwiseOr),
);
if self.at(TokenKind::Equal) {
Stmt::Assign(self.parse_assign_statement(parsed_expr, start))
@@ -309,8 +311,10 @@ impl<'src> Parser<'src> {
|parser| {
// Allow starred expression to raise a better error message for
// an invalid delete target later.
let mut target =
parser.parse_conditional_expression_or_higher(AllowStarredExpression::Yes);
let mut target = parser.parse_conditional_expression_or_higher(
ExpressionContext::default()
.with_starred_expression_allowed(StarredExpressionPrecedence::Conditional),
);
helpers::set_expr_ctx(&mut target.expr, ExprContext::Del);
// test_err invalid_del_target
@@ -356,9 +360,15 @@ impl<'src> Parser<'src> {
// return yield from x
// return x := 1
// return *x and y
let value = self
.at_expr()
.then(|| Box::new(self.parse_star_expression_list().expr));
let value = self.at_expr().then(|| {
Box::new(
self.parse_expression_list(
ExpressionContext::default()
.with_starred_expression_allowed(StarredExpressionPrecedence::BitwiseOr),
)
.expr,
)
});
ast::StmtReturn {
range: self.node_range(start),
@@ -384,7 +394,7 @@ impl<'src> Parser<'src> {
// raise *x
// raise yield x
// raise x := 1
let exc = self.parse_expression_list(AllowStarredExpression::No);
let exc = self.parse_expression_list(ExpressionContext::default());
if let Some(ast::ExprTuple {
parenthesized: false,
@@ -406,7 +416,7 @@ impl<'src> Parser<'src> {
// raise x from *y
// raise x from yield y
// raise x from y := 1
let cause = self.parse_expression_list(AllowStarredExpression::No);
let cause = self.parse_expression_list(ExpressionContext::default());
if let Some(ast::ExprTuple {
parenthesized: false,
@@ -714,7 +724,7 @@ impl<'src> Parser<'src> {
// assert assert x
// assert yield x
// assert x := 1
let test = self.parse_conditional_expression_or_higher(AllowStarredExpression::No);
let test = self.parse_conditional_expression_or_higher(ExpressionContext::default());
let msg = if self.eat(TokenKind::Comma) {
if self.at_expr() {
@@ -724,7 +734,7 @@ impl<'src> Parser<'src> {
// assert False, yield x
// assert False, x := 1
Some(Box::new(
self.parse_conditional_expression_or_higher(AllowStarredExpression::No)
self.parse_conditional_expression_or_higher(ExpressionContext::default())
.expr,
))
} else {
@@ -854,7 +864,7 @@ impl<'src> Parser<'src> {
// type x = yield y
// type x = yield from y
// type x = x := 1
let value = self.parse_conditional_expression_or_higher(AllowStarredExpression::No);
let value = self.parse_conditional_expression_or_higher(ExpressionContext::default());
ast::StmtTypeAlias {
name: Box::new(name),
@@ -1014,15 +1024,18 @@ impl<'src> Parser<'src> {
// x = *lambda x: x
// x = x := 1
let mut value = self.parse_yield_expression_or_else(Parser::parse_star_expression_list);
let context = ExpressionContext::default()
.with_yield_expression_allowed()
.with_starred_expression_allowed(StarredExpressionPrecedence::BitwiseOr);
let mut value = self.parse_expression_list(context);
if self.at(TokenKind::Equal) {
// This path is only taken when there are more than one assignment targets.
self.parse_list(RecoveryContextKind::AssignmentTargets, |parser| {
parser.bump(TokenKind::Equal);
let mut parsed_expr =
parser.parse_yield_expression_or_else(Parser::parse_star_expression_list);
let mut parsed_expr = parser.parse_expression_list(context);
std::mem::swap(&mut value, &mut parsed_expr);
@@ -1089,10 +1102,12 @@ impl<'src> Parser<'src> {
// x: yield from b = 1
// x: y := int = 1
let context = ExpressionContext::default();
// test_err ann_assign_stmt_type_alias_annotation
// a: type X = int
// lambda: type X = int
let annotation = self.parse_conditional_expression_or_higher(AllowStarredExpression::No);
let annotation = self.parse_conditional_expression_or_higher(context);
let value = if self.eat(TokenKind::Equal) {
if self.at_expr() {
@@ -1101,8 +1116,14 @@ impl<'src> Parser<'src> {
// x: Any = x := 1
// x: list = [x, *a | b, *a or b]
Some(Box::new(
self.parse_yield_expression_or_else(Parser::parse_star_expression_list)
.expr,
self.parse_expression_list(
ExpressionContext::default()
.with_yield_expression_allowed()
.with_starred_expression_allowed(
StarredExpressionPrecedence::BitwiseOr,
),
)
.expr,
))
} else {
// test_err ann_assign_stmt_missing_rhs
@@ -1170,7 +1191,11 @@ impl<'src> Parser<'src> {
// x += *yield from x
// x += *lambda x: x
// x += y := 1
let value = self.parse_yield_expression_or_else(Parser::parse_star_expression_list);
let value = self.parse_expression_list(
ExpressionContext::default()
.with_yield_expression_allowed()
.with_starred_expression_allowed(StarredExpressionPrecedence::BitwiseOr),
);
ast::StmtAugAssign {
target: Box::new(target.expr),
@@ -1198,7 +1223,7 @@ impl<'src> Parser<'src> {
// test_err if_stmt_missing_test
// if : ...
let test = self.parse_named_expression_or_higher(AllowStarredExpression::No);
let test = self.parse_named_expression_or_higher(ExpressionContext::default());
// test_err if_stmt_missing_colon
// if x
@@ -1253,7 +1278,7 @@ impl<'src> Parser<'src> {
// elif yield x:
// pass
Some(
self.parse_named_expression_or_higher(AllowStarredExpression::No)
self.parse_named_expression_or_higher(ExpressionContext::default())
.expr,
)
} else {
@@ -1414,7 +1439,7 @@ impl<'src> Parser<'src> {
// pass
// except* *x:
// pass
let parsed_expr = self.parse_expression_list(AllowStarredExpression::No);
let parsed_expr = self.parse_expression_list(ExpressionContext::default());
if matches!(
parsed_expr.expr,
Expr::Tuple(ast::ExprTuple {
@@ -1522,22 +1547,34 @@ impl<'src> Parser<'src> {
fn parse_for_statement(&mut self, start: TextSize) -> ast::StmtFor {
self.bump(TokenKind::For);
// This is to avoid the ambiguity of the `in` token which is used in
// both the `for` statement and the comparison expression. For example:
//
// ```python
// for x in y:
// # ^^^^^^
// # This is not a comparison expression
// pass
// ```
let saved_context = self.set_ctx(ParserCtxFlags::FOR_TARGET);
// test_err for_stmt_missing_target
// for in x: ...
let mut target = self.parse_expression_list(AllowStarredExpression::Yes);
self.restore_ctx(ParserCtxFlags::FOR_TARGET, saved_context);
// test_ok for_in_target_valid_expr
// for d[x in y] in target: ...
// for (x in y)[0] in iter: ...
// for (x in y).attr in iter: ...
// test_err for_stmt_invalid_target_in_keyword
// for d(x in y) in target: ...
// for (x in y)() in iter: ...
// for (x in y) in iter: ...
// for (x in y, z) in iter: ...
// for [x in y, z] in iter: ...
// for {x in y, z} in iter: ...
// test_err for_stmt_invalid_target_binary_expr
// for x not in y in z: ...
// for x == y in z: ...
// for x or y in z: ...
// for -x in y: ...
// for not x in y: ...
// for x | y in z: ...
let mut target = self.parse_expression_list(
ExpressionContext::default()
.with_starred_expression_allowed(StarredExpressionPrecedence::Conditional)
.with_in_not_included(),
);
helpers::set_expr_ctx(&mut target.expr, ExprContext::Store);
@@ -1547,6 +1584,7 @@ impl<'src> Parser<'src> {
// for *x and y in z: ...
// for *x | y in z: ...
// for await x in z: ...
// for yield x in y: ...
// for [x, 1, y, *["a"]] in z: ...
self.validate_assignment_target(&target.expr);
@@ -1563,7 +1601,10 @@ impl<'src> Parser<'src> {
// for x in *a and b: ...
// for x in yield a: ...
// for target in x := 1: ...
let iter = self.parse_star_expression_list();
let iter = self.parse_expression_list(
ExpressionContext::default()
.with_starred_expression_allowed(StarredExpressionPrecedence::BitwiseOr),
);
self.expect(TokenKind::Colon);
@@ -1607,7 +1648,7 @@ impl<'src> Parser<'src> {
// while yield x: ...
// while a, b: ...
// while a := 1, b: ...
let test = self.parse_named_expression_or_higher(AllowStarredExpression::No);
let test = self.parse_named_expression_or_higher(ExpressionContext::default());
// test_err while_stmt_missing_colon
// while (
@@ -1663,23 +1704,19 @@ impl<'src> Parser<'src> {
// x = 10
let type_params = self.try_parse_type_params();
// test_ok function_def_parameter_range
// def foo(
// first: int,
// second: int,
// ) -> int: ...
// test_err function_def_unclosed_parameter_list
// def foo(a: int, b:
// def foo():
// return 42
// def foo(a: int, b: str
// x = 10
let parameters_start = self.node_start();
self.expect(TokenKind::Lpar);
let mut parameters = self.parse_parameters(FunctionKind::FunctionDef);
self.expect(TokenKind::Rpar);
// test_ok function_def_parameter_range
// def foo(
// first: int,
// second: int,
// ) -> int: ...
parameters.range = self.node_range(parameters_start);
let parameters = self.parse_parameters(FunctionKind::FunctionDef);
let returns = if self.eat(TokenKind::Rarrow) {
if self.at_expr() {
@@ -1693,7 +1730,7 @@ impl<'src> Parser<'src> {
// def foo() -> *int: ...
// def foo() -> (*int): ...
// def foo() -> yield x: ...
let returns = self.parse_expression_list(AllowStarredExpression::No);
let returns = self.parse_expression_list(ExpressionContext::default());
if matches!(
returns.expr,
@@ -1742,7 +1779,7 @@ impl<'src> Parser<'src> {
ast::StmtFunctionDef {
name,
type_params,
type_params: type_params.map(Box::new),
parameters: Box::new(parameters),
body,
decorator_list,
@@ -2166,9 +2203,10 @@ impl<'src> Parser<'src> {
// with (a | b) << c | d: ...
// # Postfix should still be parsed first
// with (a)[0] + b * c: ...
self.parse_expression_with_precedence_recursive(
self.parse_binary_expression_or_higher_recursive(
lhs.into(),
Precedence::Initial,
OperatorPrecedence::Initial,
ExpressionContext::default(),
start,
)
.expr
@@ -2219,9 +2257,11 @@ impl<'src> Parser<'src> {
//
// Thus, we can conclude that the grammar used should be:
// (yield_expr | star_named_expression)
let parsed_expr = self.parse_yield_expression_or_else(|p| {
p.parse_star_expression_or_higher(AllowNamedExpression::Yes)
});
let parsed_expr = self.parse_named_expression_or_higher(
ExpressionContext::default()
.with_yield_expression_allowed()
.with_starred_expression_allowed(StarredExpressionPrecedence::BitwiseOr),
);
if matches!(self.current_token_kind(), TokenKind::Async | TokenKind::For) {
if parsed_expr.is_unparenthesized_starred_expr() {
@@ -2283,7 +2323,7 @@ impl<'src> Parser<'src> {
} else {
// If it's not in an ambiguous state, then the grammar of the with item
// should be used which is `expression`.
self.parse_conditional_expression_or_higher(AllowStarredExpression::No)
self.parse_conditional_expression_or_higher(ExpressionContext::default())
};
let optional_vars = self
@@ -2309,7 +2349,10 @@ impl<'src> Parser<'src> {
fn parse_with_item_optional_vars(&mut self) -> ParsedExpr {
self.bump(TokenKind::As);
let mut target = self.parse_conditional_expression_or_higher(AllowStarredExpression::Yes);
let mut target = self.parse_conditional_expression_or_higher(
ExpressionContext::default()
.with_starred_expression_allowed(StarredExpressionPrecedence::Conditional),
);
// This has the same semantics as an assignment target.
self.validate_assignment_target(&target.expr);
@@ -2340,7 +2383,10 @@ impl<'src> Parser<'src> {
//
// First try with `star_named_expression`, then if there's no comma,
// we'll restrict it to `named_expression`.
let subject = self.parse_star_expression_or_higher(AllowNamedExpression::Yes);
let subject = self.parse_named_expression_or_higher(
ExpressionContext::default()
.with_starred_expression_allowed(StarredExpressionPrecedence::BitwiseOr),
);
// test_ok match_stmt_subject_expr
// match x := 1:
@@ -2364,7 +2410,11 @@ impl<'src> Parser<'src> {
let subject = if self.at(TokenKind::Comma) {
let tuple =
self.parse_tuple_expression(subject.expr, subject_start, Parenthesized::No, |p| {
p.parse_star_expression_or_higher(AllowNamedExpression::Yes)
p.parse_named_expression_or_higher(
ExpressionContext::default().with_starred_expression_allowed(
StarredExpressionPrecedence::BitwiseOr,
),
)
});
Expr::Tuple(tuple).into()
@@ -2474,7 +2524,7 @@ impl<'src> Parser<'src> {
// match x:
// case y if yield x: ...
Some(Box::new(
self.parse_named_expression_or_higher(AllowStarredExpression::No)
self.parse_named_expression_or_higher(ExpressionContext::default())
.expr,
))
} else {
@@ -2492,7 +2542,12 @@ impl<'src> Parser<'src> {
};
self.expect(TokenKind::Colon);
let body = self.parse_body(Clause::Match);
// test_err case_expect_indented_block
// match subject:
// case 1:
// case 2: ...
let body = self.parse_body(Clause::Case);
ast::MatchCase {
pattern,
@@ -2587,7 +2642,7 @@ impl<'src> Parser<'src> {
// @yield x
// @yield from x
// def foo(): ...
let parsed_expr = self.parse_named_expression_or_higher(AllowStarredExpression::No);
let parsed_expr = self.parse_named_expression_or_higher(ExpressionContext::default());
decorators.push(ast::Decorator {
expression: parsed_expr.expr,
@@ -2743,7 +2798,11 @@ impl<'src> Parser<'src> {
// def foo(*args: *int or str): ...
// def foo(*args: *yield x): ...
// # def foo(*args: **int): ...
self.parse_star_expression_or_higher(AllowNamedExpression::No)
self.parse_conditional_expression_or_higher(
ExpressionContext::default().with_starred_expression_allowed(
StarredExpressionPrecedence::BitwiseOr,
),
)
}
AllowStarAnnotation::No => {
// test_ok param_with_annotation
@@ -2756,7 +2815,7 @@ impl<'src> Parser<'src> {
// def foo(arg: *int): ...
// def foo(arg: yield int): ...
// def foo(arg: x := int): ...
self.parse_conditional_expression_or_higher(AllowStarredExpression::No)
self.parse_conditional_expression_or_higher(ExpressionContext::default())
}
};
Some(Box::new(parsed_expr.expr))
@@ -2809,7 +2868,7 @@ impl<'src> Parser<'src> {
// def foo(x=(*int)): ...
// def foo(x=yield y): ...
Some(Box::new(
self.parse_conditional_expression_or_higher(AllowStarredExpression::No)
self.parse_conditional_expression_or_higher(ExpressionContext::default())
.expr,
))
} else {
@@ -2839,19 +2898,16 @@ impl<'src> Parser<'src> {
pub(super) fn parse_parameters(&mut self, function_kind: FunctionKind) -> ast::Parameters {
let start = self.node_start();
if matches!(function_kind, FunctionKind::FunctionDef) {
self.expect(TokenKind::Lpar);
}
// TODO(dhruvmanila): This has the same problem as `parse_match_pattern_mapping`
// has where if there are multiple kwarg or vararg, the last one will win and
// the parser will drop the previous ones. Another thing is the vararg and kwarg
// uses `Parameter` (not `ParameterWithDefault`) which means that the parser cannot
// recover well from `*args=(1, 2)`.
let mut parameters = ast::Parameters {
range: TextRange::default(),
posonlyargs: vec![],
args: vec![],
kwonlyargs: vec![],
vararg: None,
kwarg: None,
};
let mut parameters = ast::Parameters::empty(TextRange::default());
let mut seen_default_param = false; // `a=10`
let mut seen_positional_only_separator = false; // `/`
@@ -3089,6 +3145,10 @@ impl<'src> Parser<'src> {
self.add_error(ParseErrorType::ExpectedKeywordParam, star_range);
}
if matches!(function_kind, FunctionKind::FunctionDef) {
self.expect(TokenKind::Rpar);
}
parameters.range = self.node_range(start);
// test_err params_duplicate_names
@@ -3175,7 +3235,7 @@ impl<'src> Parser<'src> {
// type X[T: yield from x] = int
// type X[T: x := int] = int
Some(Box::new(
self.parse_conditional_expression_or_higher(AllowStarredExpression::No)
self.parse_conditional_expression_or_higher(ExpressionContext::default())
.expr,
))
} else {
@@ -3363,7 +3423,7 @@ enum Clause {
Class,
While,
FunctionDef,
Match,
Case,
Try,
Except,
Finally,
@@ -3380,7 +3440,7 @@ impl Display for Clause {
Clause::Class => write!(f, "`class` definition"),
Clause::While => write!(f, "`while` statement"),
Clause::FunctionDef => write!(f, "function definition"),
Clause::Match => write!(f, "`match` statement"),
Clause::Case => write!(f, "`case` block"),
Clause::Try => write!(f, "`try` statement"),
Clause::Except => write!(f, "`except` clause"),
Clause::Finally => write!(f, "`finally` clause"),

View File

@@ -715,31 +715,43 @@ impl TokenKind {
)
}
/// Returns `true` if the current token is a boolean operator.
#[inline]
pub const fn is_bool_operator(&self) -> bool {
matches!(self, TokenKind::And | TokenKind::Or)
self.as_bool_operator().is_some()
}
/// Returns the [`BoolOp`] that corresponds to this token kind, if it is a boolean operator,
/// otherwise return [None].
#[inline]
pub const fn as_bool_operator(&self) -> Option<BoolOp> {
Some(match self {
TokenKind::And => BoolOp::And,
TokenKind::Or => BoolOp::Or,
_ => return None,
})
}
/// Returns the [`Operator`] that corresponds to this token kind, if it is
/// an augmented assignment operator, or [`None`] otherwise.
#[inline]
pub const fn as_augmented_assign_operator(&self) -> Option<Operator> {
match self {
TokenKind::PlusEqual => Some(Operator::Add),
TokenKind::MinusEqual => Some(Operator::Sub),
TokenKind::StarEqual => Some(Operator::Mult),
TokenKind::AtEqual => Some(Operator::MatMult),
TokenKind::DoubleStarEqual => Some(Operator::Pow),
TokenKind::SlashEqual => Some(Operator::Div),
TokenKind::DoubleSlashEqual => Some(Operator::FloorDiv),
TokenKind::PercentEqual => Some(Operator::Mod),
TokenKind::AmperEqual => Some(Operator::BitAnd),
TokenKind::VbarEqual => Some(Operator::BitOr),
TokenKind::CircumflexEqual => Some(Operator::BitXor),
TokenKind::LeftShiftEqual => Some(Operator::LShift),
TokenKind::RightShiftEqual => Some(Operator::RShift),
_ => None,
}
Some(match self {
TokenKind::PlusEqual => Operator::Add,
TokenKind::MinusEqual => Operator::Sub,
TokenKind::StarEqual => Operator::Mult,
TokenKind::AtEqual => Operator::MatMult,
TokenKind::DoubleStarEqual => Operator::Pow,
TokenKind::SlashEqual => Operator::Div,
TokenKind::DoubleSlashEqual => Operator::FloorDiv,
TokenKind::PercentEqual => Operator::Mod,
TokenKind::AmperEqual => Operator::BitAnd,
TokenKind::VbarEqual => Operator::BitOr,
TokenKind::CircumflexEqual => Operator::BitXor,
TokenKind::LeftShiftEqual => Operator::LShift,
TokenKind::RightShiftEqual => Operator::RShift,
_ => return None,
})
}
pub const fn from_token(token: &Tok) -> Self {
@@ -888,18 +900,6 @@ impl TryFrom<TokenKind> for Operator {
}
}
impl TryFrom<TokenKind> for BoolOp {
type Error = ();
fn try_from(value: TokenKind) -> Result<Self, Self::Error> {
Ok(match value {
TokenKind::And => BoolOp::And,
TokenKind::Or => BoolOp::Or,
_ => return Err(()),
})
}
}
impl TryFrom<&Tok> for UnaryOp {
type Error = String;
@@ -922,6 +922,16 @@ impl TryFrom<TokenKind> for UnaryOp {
}
}
impl From<BoolOp> for TokenKind {
#[inline]
fn from(op: BoolOp) -> Self {
match op {
BoolOp::And => TokenKind::And,
BoolOp::Or => TokenKind::Or,
}
}
}
impl fmt::Display for TokenKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = match self {

View File

@@ -0,0 +1,84 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/case_expect_indented_block.py
---
## AST
```
Module(
ModModule {
range: 0..43,
body: [
Match(
StmtMatch {
range: 0..42,
subject: Name(
ExprName {
range: 6..13,
id: "subject",
ctx: Load,
},
),
cases: [
MatchCase {
range: 19..26,
pattern: MatchValue(
PatternMatchValue {
range: 24..25,
value: NumberLiteral(
ExprNumberLiteral {
range: 24..25,
value: Int(
1,
),
},
),
},
),
guard: None,
body: [],
},
MatchCase {
range: 31..42,
pattern: MatchValue(
PatternMatchValue {
range: 36..37,
value: NumberLiteral(
ExprNumberLiteral {
range: 36..37,
value: Int(
2,
),
},
),
},
),
guard: None,
body: [
Expr(
StmtExpr {
range: 39..42,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 39..42,
},
),
},
),
],
},
],
},
),
],
},
)
```
## Errors
|
1 | match subject:
2 | case 1:
3 | case 2: ...
| ^^^^ Syntax Error: Expected an indented block after `case` block
|

View File

@@ -346,24 +346,29 @@ Module(
ExprList {
range: 187..199,
elts: [
Starred(
ExprStarred {
range: 188..190,
value: Name(
ExprName {
range: 189..190,
id: "x",
ctx: Load,
Named(
ExprNamed {
range: 188..195,
target: Starred(
ExprStarred {
range: 188..190,
value: Name(
ExprName {
range: 189..190,
id: "x",
ctx: Store,
},
),
ctx: Store,
},
),
ctx: Load,
},
),
NumberLiteral(
ExprNumberLiteral {
range: 194..195,
value: Int(
2,
value: NumberLiteral(
ExprNumberLiteral {
range: 194..195,
value: Int(
2,
),
},
),
},
),
@@ -458,5 +463,5 @@ Module(
8 | [*x if True else y, z]
9 | [*lambda x: x, z]
10 | [*x := 2, z]
| ^^ Syntax Error: Expected ',', found ':='
| ^^ Syntax Error: Assignment expression target must be an identifier
|

View File

@@ -84,30 +84,30 @@ Module(
),
Expr(
StmtExpr {
range: 81..84,
value: Starred(
ExprStarred {
range: 82..84,
value: Name(
ExprName {
range: 83..84,
id: "x",
ctx: Load,
range: 81..90,
value: Named(
ExprNamed {
range: 82..89,
target: Starred(
ExprStarred {
range: 82..84,
value: Name(
ExprName {
range: 83..84,
id: "x",
ctx: Store,
},
),
ctx: Store,
},
),
ctx: Load,
},
),
},
),
Expr(
StmtExpr {
range: 88..89,
value: NumberLiteral(
ExprNumberLiteral {
range: 88..89,
value: Int(
1,
value: NumberLiteral(
ExprNumberLiteral {
range: 88..89,
value: Int(
1,
),
},
),
},
),
@@ -198,34 +198,7 @@ Module(
3 | (x.y := 1)
4 | (x[y] := 1)
5 | (*x := 1)
| ^^ Syntax Error: Starred expression cannot be used here
6 | ([x, y] := [1, 2])
|
|
3 | (x.y := 1)
4 | (x[y] := 1)
5 | (*x := 1)
| ^^ Syntax Error: Expected ')', found ':='
6 | ([x, y] := [1, 2])
|
|
3 | (x.y := 1)
4 | (x[y] := 1)
5 | (*x := 1)
| ^ Syntax Error: Expected a statement
6 | ([x, y] := [1, 2])
|
|
3 | (x.y := 1)
4 | (x[y] := 1)
5 | (*x := 1)
| ^ Syntax Error: Expected a statement
| ^^ Syntax Error: Assignment expression target must be an identifier
6 | ([x, y] := [1, 2])
|

View File

@@ -491,34 +491,34 @@ Module(
),
Expr(
StmtExpr {
range: 323..326,
value: Starred(
ExprStarred {
range: 324..326,
value: Name(
ExprName {
range: 325..326,
id: "x",
ctx: Load,
},
),
ctx: Load,
},
),
},
),
Expr(
StmtExpr {
range: 330..343,
range: 323..344,
value: Tuple(
ExprTuple {
range: 330..343,
range: 323..344,
elts: [
NumberLiteral(
ExprNumberLiteral {
range: 330..331,
value: Int(
2,
Named(
ExprNamed {
range: 324..331,
target: Starred(
ExprStarred {
range: 324..326,
value: Name(
ExprName {
range: 325..326,
id: "x",
ctx: Store,
},
),
ctx: Store,
},
),
value: NumberLiteral(
ExprNumberLiteral {
range: 330..331,
value: Int(
2,
),
},
),
},
),
@@ -529,30 +529,35 @@ Module(
ctx: Load,
},
),
Starred(
ExprStarred {
range: 336..338,
value: Name(
ExprName {
range: 337..338,
id: "x",
ctx: Load,
Named(
ExprNamed {
range: 336..343,
target: Starred(
ExprStarred {
range: 336..338,
value: Name(
ExprName {
range: 337..338,
id: "x",
ctx: Store,
},
),
ctx: Store,
},
),
ctx: Load,
},
),
NumberLiteral(
ExprNumberLiteral {
range: 342..343,
value: Int(
2,
value: NumberLiteral(
ExprNumberLiteral {
range: 342..343,
value: Int(
2,
),
},
),
},
),
],
ctx: Load,
parenthesized: false,
parenthesized: true,
},
),
},
@@ -1231,7 +1236,7 @@ Module(
8 | (*x if True else y, z, *x if True else y)
9 | (*lambda x: x, z, *lambda x: x)
10 | (*x := 2, z, *x := 2)
| ^^ Syntax Error: Starred expression cannot be used here
| ^^ Syntax Error: Assignment expression target must be an identifier
|
@@ -1239,34 +1244,7 @@ Module(
8 | (*x if True else y, z, *x if True else y)
9 | (*lambda x: x, z, *lambda x: x)
10 | (*x := 2, z, *x := 2)
| ^^ Syntax Error: Expected ')', found ':='
|
|
8 | (*x if True else y, z, *x if True else y)
9 | (*lambda x: x, z, *lambda x: x)
10 | (*x := 2, z, *x := 2)
| ^^ Syntax Error: Expected ',', found ':='
|
|
8 | (*x if True else y, z, *x if True else y)
9 | (*lambda x: x, z, *lambda x: x)
10 | (*x := 2, z, *x := 2)
| ^ Syntax Error: Expected a statement
|
|
8 | (*x if True else y, z, *x if True else y)
9 | (*lambda x: x, z, *lambda x: x)
10 | (*x := 2, z, *x := 2)
| ^ Syntax Error: Expected a statement
11 |
12 |
13 | # Non-parenthesized
| ^^ Syntax Error: Assignment expression target must be an identifier
|

View File

@@ -339,24 +339,29 @@ Module(
ExprSet {
range: 186..198,
elts: [
Starred(
ExprStarred {
range: 187..189,
value: Name(
ExprName {
range: 188..189,
id: "x",
ctx: Load,
Named(
ExprNamed {
range: 187..194,
target: Starred(
ExprStarred {
range: 187..189,
value: Name(
ExprName {
range: 188..189,
id: "x",
ctx: Store,
},
),
ctx: Store,
},
),
ctx: Load,
},
),
NumberLiteral(
ExprNumberLiteral {
range: 193..194,
value: Int(
2,
value: NumberLiteral(
ExprNumberLiteral {
range: 193..194,
value: Int(
2,
),
},
),
},
),
@@ -450,5 +455,5 @@ Module(
8 | {*x if True else y, z}
9 | {*lambda x: x, z}
10 | {*x := 2, z}
| ^^ Syntax Error: Expected ',', found ':='
| ^^ Syntax Error: Assignment expression target must be an identifier
|

View File

@@ -1,89 +0,0 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/for_in_target_postfix_expr.py
---
## AST
```
Module(
ModModule {
range: 0..29,
body: [
For(
StmtFor {
range: 0..28,
is_async: false,
target: Call(
ExprCall {
range: 4..13,
func: Name(
ExprName {
range: 4..5,
id: "d",
ctx: Load,
},
),
arguments: Arguments {
range: 5..13,
args: [
Compare(
ExprCompare {
range: 6..12,
left: Name(
ExprName {
range: 6..7,
id: "x",
ctx: Load,
},
),
ops: [
In,
],
comparators: [
Name(
ExprName {
range: 11..12,
id: "y",
ctx: Load,
},
),
],
},
),
],
keywords: [],
},
},
),
iter: Name(
ExprName {
range: 17..23,
id: "target",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 25..28,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 25..28,
},
),
},
),
],
orelse: [],
},
),
],
},
)
```
## Errors
|
1 | for d(x in y) in target: ...
| ^^^^^^^^^ Syntax Error: Invalid assignment target
|

View File

@@ -7,7 +7,7 @@ input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_invalid_targ
```
Module(
ModModule {
range: 0..132,
range: 0..154,
body: [
For(
StmtFor {
@@ -233,22 +233,79 @@ Module(
),
For(
StmtFor {
range: 100..131,
range: 100..121,
is_async: false,
target: Yield(
ExprYield {
range: 104..116,
value: Some(
Compare(
ExprCompare {
range: 110..116,
left: Name(
ExprName {
range: 110..111,
id: "x",
ctx: Load,
},
),
ops: [
In,
],
comparators: [
Name(
ExprName {
range: 115..116,
id: "y",
ctx: Load,
},
),
],
},
),
),
},
),
iter: Name(
ExprName {
range: 116..116,
id: "",
ctx: Invalid,
},
),
body: [
Expr(
StmtExpr {
range: 118..121,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 118..121,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 122..153,
is_async: false,
target: List(
ExprList {
range: 104..121,
range: 126..143,
elts: [
Name(
ExprName {
range: 105..106,
range: 127..128,
id: "x",
ctx: Store,
},
),
NumberLiteral(
ExprNumberLiteral {
range: 108..109,
range: 130..131,
value: Int(
1,
),
@@ -256,25 +313,25 @@ Module(
),
Name(
ExprName {
range: 111..112,
range: 133..134,
id: "y",
ctx: Store,
},
),
Starred(
ExprStarred {
range: 114..120,
range: 136..142,
value: List(
ExprList {
range: 115..120,
range: 137..142,
elts: [
StringLiteral(
ExprStringLiteral {
range: 116..119,
range: 138..141,
value: StringLiteralValue {
inner: Single(
StringLiteral {
range: 116..119,
range: 138..141,
value: "a",
flags: StringLiteralFlags {
quote_style: Double,
@@ -299,7 +356,7 @@ Module(
),
iter: Name(
ExprName {
range: 125..126,
range: 147..148,
id: "z",
ctx: Load,
},
@@ -307,10 +364,10 @@ Module(
body: [
Expr(
StmtExpr {
range: 128..131,
range: 150..153,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 128..131,
range: 150..153,
},
),
},
@@ -358,7 +415,7 @@ Module(
4 | for *x | y in z: ...
| ^^^^^ Syntax Error: Invalid assignment target
5 | for await x in z: ...
6 | for [x, 1, y, *["a"]] in z: ...
6 | for yield x in y: ...
|
@@ -367,21 +424,40 @@ Module(
4 | for *x | y in z: ...
5 | for await x in z: ...
| ^^^^^^^ Syntax Error: Invalid assignment target
6 | for [x, 1, y, *["a"]] in z: ...
6 | for yield x in y: ...
7 | for [x, 1, y, *["a"]] in z: ...
|
|
4 | for *x | y in z: ...
5 | for await x in z: ...
6 | for [x, 1, y, *["a"]] in z: ...
6 | for yield x in y: ...
| ^^^^^^^^^^^^ Syntax Error: Yield expression cannot be used here
7 | for [x, 1, y, *["a"]] in z: ...
|
|
4 | for *x | y in z: ...
5 | for await x in z: ...
6 | for yield x in y: ...
| ^ Syntax Error: Expected 'in', found ':'
7 | for [x, 1, y, *["a"]] in z: ...
|
|
5 | for await x in z: ...
6 | for yield x in y: ...
7 | for [x, 1, y, *["a"]] in z: ...
| ^ Syntax Error: Invalid assignment target
|
|
4 | for *x | y in z: ...
5 | for await x in z: ...
6 | for [x, 1, y, *["a"]] in z: ...
6 | for yield x in y: ...
7 | for [x, 1, y, *["a"]] in z: ...
| ^^^ Syntax Error: Invalid assignment target
|

View File

@@ -0,0 +1,341 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_invalid_target_binary_expr.py
---
## AST
```
Module(
ModModule {
range: 0..124,
body: [
For(
StmtFor {
range: 0..24,
is_async: false,
target: Compare(
ExprCompare {
range: 4..14,
left: Name(
ExprName {
range: 4..5,
id: "x",
ctx: Load,
},
),
ops: [
NotIn,
],
comparators: [
Name(
ExprName {
range: 13..14,
id: "y",
ctx: Load,
},
),
],
},
),
iter: Name(
ExprName {
range: 18..19,
id: "z",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 21..24,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 21..24,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 25..45,
is_async: false,
target: Compare(
ExprCompare {
range: 29..35,
left: Name(
ExprName {
range: 29..30,
id: "x",
ctx: Load,
},
),
ops: [
Eq,
],
comparators: [
Name(
ExprName {
range: 34..35,
id: "y",
ctx: Load,
},
),
],
},
),
iter: Name(
ExprName {
range: 39..40,
id: "z",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 42..45,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 42..45,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 46..66,
is_async: false,
target: BoolOp(
ExprBoolOp {
range: 50..56,
op: Or,
values: [
Name(
ExprName {
range: 50..51,
id: "x",
ctx: Load,
},
),
Name(
ExprName {
range: 55..56,
id: "y",
ctx: Load,
},
),
],
},
),
iter: Name(
ExprName {
range: 60..61,
id: "z",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 63..66,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 63..66,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 67..83,
is_async: false,
target: UnaryOp(
ExprUnaryOp {
range: 71..73,
op: USub,
operand: Name(
ExprName {
range: 72..73,
id: "x",
ctx: Store,
},
),
},
),
iter: Name(
ExprName {
range: 77..78,
id: "y",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 80..83,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 80..83,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 84..103,
is_async: false,
target: UnaryOp(
ExprUnaryOp {
range: 88..93,
op: Not,
operand: Name(
ExprName {
range: 92..93,
id: "x",
ctx: Store,
},
),
},
),
iter: Name(
ExprName {
range: 97..98,
id: "y",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 100..103,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 100..103,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 104..123,
is_async: false,
target: BinOp(
ExprBinOp {
range: 108..113,
left: Name(
ExprName {
range: 108..109,
id: "x",
ctx: Load,
},
),
op: BitOr,
right: Name(
ExprName {
range: 112..113,
id: "y",
ctx: Load,
},
),
},
),
iter: Name(
ExprName {
range: 117..118,
id: "z",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 120..123,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 120..123,
},
),
},
),
],
orelse: [],
},
),
],
},
)
```
## Errors
|
1 | for x not in y in z: ...
| ^^^^^^^^^^ Syntax Error: Invalid assignment target
2 | for x == y in z: ...
3 | for x or y in z: ...
|
|
1 | for x not in y in z: ...
2 | for x == y in z: ...
| ^^^^^^ Syntax Error: Invalid assignment target
3 | for x or y in z: ...
4 | for -x in y: ...
|
|
1 | for x not in y in z: ...
2 | for x == y in z: ...
3 | for x or y in z: ...
| ^^^^^^ Syntax Error: Invalid assignment target
4 | for -x in y: ...
5 | for not x in y: ...
|
|
2 | for x == y in z: ...
3 | for x or y in z: ...
4 | for -x in y: ...
| ^^ Syntax Error: Invalid assignment target
5 | for not x in y: ...
6 | for x | y in z: ...
|
|
3 | for x or y in z: ...
4 | for -x in y: ...
5 | for not x in y: ...
| ^^^^^ Syntax Error: Invalid assignment target
6 | for x | y in z: ...
|
|
4 | for -x in y: ...
5 | for not x in y: ...
6 | for x | y in z: ...
| ^^^^^ Syntax Error: Invalid assignment target
|

View File

@@ -1,27 +1,95 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/parenthesized_compare_expr_in_for.py
input_file: crates/ruff_python_parser/resources/inline/err/for_stmt_invalid_target_in_keyword.py
---
## AST
```
Module(
ModModule {
range: 0..141,
range: 0..170,
body: [
For(
StmtFor {
range: 0..27,
range: 0..28,
is_async: false,
target: Call(
ExprCall {
range: 4..14,
range: 4..13,
func: Name(
ExprName {
range: 4..5,
id: "d",
ctx: Load,
},
),
arguments: Arguments {
range: 5..13,
args: [
Compare(
ExprCompare {
range: 6..12,
left: Name(
ExprName {
range: 6..7,
id: "x",
ctx: Load,
},
),
ops: [
In,
],
comparators: [
Name(
ExprName {
range: 11..12,
id: "y",
ctx: Load,
},
),
],
},
),
],
keywords: [],
},
},
),
iter: Name(
ExprName {
range: 17..23,
id: "target",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 25..28,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 25..28,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 29..56,
is_async: false,
target: Call(
ExprCall {
range: 33..43,
func: Compare(
ExprCompare {
range: 5..11,
range: 34..40,
left: Name(
ExprName {
range: 5..6,
range: 34..35,
id: "x",
ctx: Load,
},
@@ -32,7 +100,7 @@ Module(
comparators: [
Name(
ExprName {
range: 10..11,
range: 39..40,
id: "y",
ctx: Load,
},
@@ -41,7 +109,7 @@ Module(
},
),
arguments: Arguments {
range: 12..14,
range: 41..43,
args: [],
keywords: [],
},
@@ -49,7 +117,7 @@ Module(
),
iter: Name(
ExprName {
range: 18..22,
range: 47..51,
id: "iter",
ctx: Load,
},
@@ -57,10 +125,10 @@ Module(
body: [
Expr(
StmtExpr {
range: 24..27,
range: 53..56,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 24..27,
range: 53..56,
},
),
},
@@ -71,14 +139,14 @@ Module(
),
For(
StmtFor {
range: 28..53,
range: 57..82,
is_async: false,
target: Compare(
ExprCompare {
range: 33..39,
range: 62..68,
left: Name(
ExprName {
range: 33..34,
range: 62..63,
id: "x",
ctx: Load,
},
@@ -89,7 +157,7 @@ Module(
comparators: [
Name(
ExprName {
range: 38..39,
range: 67..68,
id: "y",
ctx: Load,
},
@@ -97,72 +165,6 @@ Module(
],
},
),
iter: Name(
ExprName {
range: 44..48,
id: "iter",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 50..53,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 50..53,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 54..82,
is_async: false,
target: Tuple(
ExprTuple {
range: 58..69,
elts: [
Compare(
ExprCompare {
range: 59..65,
left: Name(
ExprName {
range: 59..60,
id: "x",
ctx: Load,
},
),
ops: [
In,
],
comparators: [
Name(
ExprName {
range: 64..65,
id: "y",
ctx: Load,
},
),
],
},
),
Name(
ExprName {
range: 67..68,
id: "z",
ctx: Store,
},
),
],
ctx: Store,
parenthesized: true,
},
),
iter: Name(
ExprName {
range: 73..77,
@@ -189,8 +191,8 @@ Module(
StmtFor {
range: 83..111,
is_async: false,
target: List(
ExprList {
target: Tuple(
ExprTuple {
range: 87..98,
elts: [
Compare(
@@ -226,6 +228,7 @@ Module(
),
],
ctx: Store,
parenthesized: true,
},
),
iter: Name(
@@ -254,8 +257,8 @@ Module(
StmtFor {
range: 112..140,
is_async: false,
target: Set(
ExprSet {
target: List(
ExprList {
range: 116..127,
elts: [
Compare(
@@ -286,10 +289,11 @@ Module(
ExprName {
range: 125..126,
id: "z",
ctx: Load,
ctx: Store,
},
),
],
ctx: Store,
},
),
iter: Name(
@@ -314,6 +318,70 @@ Module(
orelse: [],
},
),
For(
StmtFor {
range: 141..169,
is_async: false,
target: Set(
ExprSet {
range: 145..156,
elts: [
Compare(
ExprCompare {
range: 146..152,
left: Name(
ExprName {
range: 146..147,
id: "x",
ctx: Load,
},
),
ops: [
In,
],
comparators: [
Name(
ExprName {
range: 151..152,
id: "y",
ctx: Load,
},
),
],
},
),
Name(
ExprName {
range: 154..155,
id: "z",
ctx: Load,
},
),
],
},
),
iter: Name(
ExprName {
range: 160..164,
id: "iter",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 166..169,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 166..169,
},
),
},
),
],
orelse: [],
},
),
],
},
)
@@ -321,44 +389,54 @@ Module(
## Errors
|
1 | for (x in y)() in iter: ...
1 | for d(x in y) in target: ...
| ^^^^^^^^^ Syntax Error: Invalid assignment target
2 | for (x in y)() in iter: ...
3 | for (x in y) in iter: ...
|
|
1 | for d(x in y) in target: ...
2 | for (x in y)() in iter: ...
| ^^^^^^^^^^ Syntax Error: Invalid assignment target
2 | for (x in y) in iter: ...
3 | for (x in y, z) in iter: ...
3 | for (x in y) in iter: ...
4 | for (x in y, z) in iter: ...
|
|
1 | for (x in y)() in iter: ...
2 | for (x in y) in iter: ...
1 | for d(x in y) in target: ...
2 | for (x in y)() in iter: ...
3 | for (x in y) in iter: ...
| ^^^^^^ Syntax Error: Invalid assignment target
3 | for (x in y, z) in iter: ...
4 | for [x in y, z] in iter: ...
4 | for (x in y, z) in iter: ...
5 | for [x in y, z] in iter: ...
|
|
1 | for (x in y)() in iter: ...
2 | for (x in y) in iter: ...
3 | for (x in y, z) in iter: ...
2 | for (x in y)() in iter: ...
3 | for (x in y) in iter: ...
4 | for (x in y, z) in iter: ...
| ^^^^^^ Syntax Error: Invalid assignment target
4 | for [x in y, z] in iter: ...
5 | for {x in y, z} in iter: ...
5 | for [x in y, z] in iter: ...
6 | for {x in y, z} in iter: ...
|
|
2 | for (x in y) in iter: ...
3 | for (x in y, z) in iter: ...
4 | for [x in y, z] in iter: ...
3 | for (x in y) in iter: ...
4 | for (x in y, z) in iter: ...
5 | for [x in y, z] in iter: ...
| ^^^^^^ Syntax Error: Invalid assignment target
5 | for {x in y, z} in iter: ...
6 | for {x in y, z} in iter: ...
|
|
3 | for (x in y, z) in iter: ...
4 | for [x in y, z] in iter: ...
5 | for {x in y, z} in iter: ...
4 | for (x in y, z) in iter: ...
5 | for [x in y, z] in iter: ...
6 | for {x in y, z} in iter: ...
| ^^^^^^^^^^^ Syntax Error: Invalid assignment target
|

View File

@@ -0,0 +1,122 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/node_range_with_gaps.py
---
## AST
```
Module(
ModModule {
range: 0..41,
body: [
FunctionDef(
StmtFunctionDef {
range: 0..7,
is_async: false,
decorator_list: [],
name: Identifier {
id: "foo",
range: 4..7,
},
type_params: None,
parameters: Parameters {
range: 7..7,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [],
},
),
FunctionDef(
StmtFunctionDef {
range: 18..32,
is_async: false,
decorator_list: [],
name: Identifier {
id: "bar",
range: 22..25,
},
type_params: None,
parameters: Parameters {
range: 25..27,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [
Expr(
StmtExpr {
range: 29..32,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 29..32,
},
),
},
),
],
},
),
FunctionDef(
StmtFunctionDef {
range: 33..40,
is_async: false,
decorator_list: [],
name: Identifier {
id: "baz",
range: 37..40,
},
type_params: None,
parameters: Parameters {
range: 40..40,
posonlyargs: [],
args: [],
vararg: None,
kwonlyargs: [],
kwarg: None,
},
returns: None,
body: [],
},
),
],
},
)
```
## Errors
|
1 | def foo # comment
| ^ Syntax Error: Expected '(', found newline
2 | def bar(): ...
3 | def baz
|
|
1 | def foo # comment
2 | def bar(): ...
| ^^^ Syntax Error: Expected ')', found 'def'
3 | def baz
|
|
1 | def foo # comment
2 | def bar(): ...
3 | def baz
| ^ Syntax Error: Expected '(', found newline
|
|
2 | def bar(): ...
3 | def baz
|

View File

@@ -167,7 +167,7 @@ Module(
conversion: None,
format_spec: Some(
FStringFormatSpec {
range: 43..43,
range: 42..42,
elements: [],
},
),

View File

@@ -1,78 +0,0 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/for_in_target_postfix_expr.py
---
## AST
```
Module(
ModModule {
range: 0..29,
body: [
For(
StmtFor {
range: 0..28,
is_async: false,
target: Subscript(
ExprSubscript {
range: 4..13,
value: Name(
ExprName {
range: 4..5,
id: "d",
ctx: Load,
},
),
slice: Compare(
ExprCompare {
range: 6..12,
left: Name(
ExprName {
range: 6..7,
id: "x",
ctx: Load,
},
),
ops: [
In,
],
comparators: [
Name(
ExprName {
range: 11..12,
id: "y",
ctx: Load,
},
),
],
},
),
ctx: Store,
},
),
iter: Name(
ExprName {
range: 17..23,
id: "target",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 25..28,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 25..28,
},
),
},
),
],
orelse: [],
},
),
],
},
)
```

View File

@@ -1,13 +1,13 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/parenthesized_compare_expr_in_for.py
input_file: crates/ruff_python_parser/resources/inline/ok/for_in_target_valid_expr.py
---
## AST
```
Module(
ModModule {
range: 0..60,
range: 0..89,
body: [
For(
StmtFor {
@@ -15,13 +15,20 @@ Module(
is_async: false,
target: Subscript(
ExprSubscript {
range: 4..15,
value: Compare(
range: 4..13,
value: Name(
ExprName {
range: 4..5,
id: "d",
ctx: Load,
},
),
slice: Compare(
ExprCompare {
range: 5..11,
range: 6..12,
left: Name(
ExprName {
range: 5..6,
range: 6..7,
id: "x",
ctx: Load,
},
@@ -32,7 +39,7 @@ Module(
comparators: [
Name(
ExprName {
range: 10..11,
range: 11..12,
id: "y",
ctx: Load,
},
@@ -40,21 +47,13 @@ Module(
],
},
),
slice: NumberLiteral(
ExprNumberLiteral {
range: 13..14,
value: Int(
0,
),
},
),
ctx: Store,
},
),
iter: Name(
ExprName {
range: 19..23,
id: "iter",
range: 17..23,
id: "target",
ctx: Load,
},
),
@@ -75,11 +74,11 @@ Module(
),
For(
StmtFor {
range: 29..59,
range: 29..57,
is_async: false,
target: Attribute(
ExprAttribute {
range: 33..46,
target: Subscript(
ExprSubscript {
range: 33..44,
value: Compare(
ExprCompare {
range: 34..40,
@@ -104,16 +103,20 @@ Module(
],
},
),
attr: Identifier {
id: "attr",
range: 42..46,
},
slice: NumberLiteral(
ExprNumberLiteral {
range: 42..43,
value: Int(
0,
),
},
),
ctx: Store,
},
),
iter: Name(
ExprName {
range: 50..54,
range: 48..52,
id: "iter",
ctx: Load,
},
@@ -121,10 +124,70 @@ Module(
body: [
Expr(
StmtExpr {
range: 56..59,
range: 54..57,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 56..59,
range: 54..57,
},
),
},
),
],
orelse: [],
},
),
For(
StmtFor {
range: 58..88,
is_async: false,
target: Attribute(
ExprAttribute {
range: 62..75,
value: Compare(
ExprCompare {
range: 63..69,
left: Name(
ExprName {
range: 63..64,
id: "x",
ctx: Load,
},
),
ops: [
In,
],
comparators: [
Name(
ExprName {
range: 68..69,
id: "y",
ctx: Load,
},
),
],
},
),
attr: Identifier {
id: "attr",
range: 71..75,
},
ctx: Store,
},
),
iter: Name(
ExprName {
range: 79..83,
id: "iter",
ctx: Load,
},
),
body: [
Expr(
StmtExpr {
range: 85..88,
value: EllipsisLiteral(
ExprEllipsisLiteral {
range: 85..88,
},
),
},

View File

@@ -7,7 +7,7 @@
1. Finally, add this to your `init.lua`:
```lua
require('lspconfig').ruff.setup()
require('lspconfig').ruff.setup {}
```
See [`nvim-lspconfig`'s server configuration guide](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#ruff) for more details

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_shrinking"
version = "0.4.0"
version = "0.4.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -2257,7 +2257,31 @@ pub struct IsortOptions {
// Tables are required to go last.
/// A list of mappings from section names to modules.
/// By default custom sections are output last, but this can be overridden with `section-order`.
///
/// By default, imports are categorized according to their type (e.g., `future`, `third-party`,
/// and so on). This setting allows you to group modules into custom sections, to augment or
/// override the built-in sections.
///
/// For example, to group all testing utilities, you could create a `testing` section:
/// ```toml
/// testing = ["pytest", "hypothesis"]
/// ```
///
/// Custom sections should typically be inserted into the `section-order` list to ensure that
/// they're displayed as a standalone group and in the intended order, as in:
/// ```toml
/// section-order = [
/// "future",
/// "standard-library",
/// "third-party",
/// "first-party",
/// "local-folder",
/// "testing"
/// ]
/// ```
///
/// If a custom section is omitted from `section-order`, imports in that section will be
/// assigned to the `default-section` (which defaults to `third-party`).
#[option(
default = "{}",
value_type = "dict[str, list[str]]",

View File

@@ -14,7 +14,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.0
rev: v0.4.1
hooks:
# Run the linter.
- id: ruff
@@ -27,7 +27,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.0
rev: v0.4.1
hooks:
# Run the linter.
- id: ruff
@@ -41,7 +41,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.4.0
rev: v0.4.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.4.0"
version = "0.4.1"
description = "An extremely fast Python linter and code formatter, written in Rust."
authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }]
readme = "README.md"

4
ruff.schema.json generated
View File

@@ -1698,7 +1698,7 @@
}
},
"sections": {
"description": "A list of mappings from section names to modules. By default custom sections are output last, but this can be overridden with `section-order`.",
"description": "A list of mappings from section names to modules.\n\nBy default, imports are categorized according to their type (e.g., `future`, `third-party`, and so on). This setting allows you to group modules into custom sections, to augment or override the built-in sections.\n\nFor example, to group all testing utilities, you could create a `testing` section: ```toml testing = [\"pytest\", \"hypothesis\"] ```\n\nCustom sections should typically be inserted into the `section-order` list to ensure that they're displayed as a standalone group and in the intended order, as in: ```toml section-order = [ \"future\", \"standard-library\", \"third-party\", \"first-party\", \"local-folder\", \"testing\" ] ```\n\nIf a custom section is omitted from `section-order`, imports in that section will be assigned to the `default-section` (which defaults to `third-party`).",
"type": [
"object",
"null"
@@ -3269,8 +3269,10 @@
"PLE0302",
"PLE0303",
"PLE0304",
"PLE0305",
"PLE0307",
"PLE0308",
"PLE0309",
"PLE06",
"PLE060",
"PLE0604",

View File

@@ -128,14 +128,14 @@ files = [
[[package]]
name = "dill"
version = "0.3.7"
version = "0.3.8"
description = "serialize all of Python"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"},
{file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"},
{file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"},
{file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"},
]
[package.extras]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "scripts"
version = "0.4.0"
version = "0.4.1"
description = ""
authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]

View File

@@ -444,7 +444,7 @@ async def main(
if matches is None:
# Handle case where there are no regex matches e.g.
# + "?application=AIRFLOW&authenticator=TEST_AUTH&role=TEST_ROLE&warehouse=TEST_WAREHOUSE" # noqa: E501, ERA001
# + "?application=AIRFLOW&authenticator=TEST_AUTH&role=TEST_ROLE&warehouse=TEST_WAREHOUSE" # noqa: E501
# Which was found in local testing
continue

244
scripts/fuzz-parser/fuzz.py Normal file
View File

@@ -0,0 +1,244 @@
"""
Run the parser on randomly generated (but syntactically valid) Python source-code files.
To install all dependencies for this script into an environment using `uv`, run:
uv pip install -r scripts/fuzz-parser/requirements.txt
Example invocations of the script:
- Run the fuzzer using seeds 0, 1, 2, 78 and 93 to generate the code:
`python scripts/fuzz-parser/fuzz.py 0-2 78 93`
- Run the fuzzer concurrently using seeds in range 0-10 inclusive,
but only reporting bugs that are new on your branch:
`python scripts/fuzz-parser/fuzz.py 0-10 --new-bugs-only`
- Run the fuzzer concurrently on 10,000 different Python source-code files,
and only print a summary at the end:
`python scripts/fuzz-parser/fuzz.py 1-10000 --quiet
N.B. The script takes a few seconds to get started, as the script needs to compile
your checked out version of ruff with `--release` as a first step before it
can actually start fuzzing.
"""
from __future__ import annotations
import argparse
import concurrent.futures
import subprocess
from dataclasses import KW_ONLY, dataclass
from typing import NewType
from pysource_codegen import generate as generate_random_code
from pysource_minimize import minimize as minimize_repro
from termcolor import colored
MinimizedSourceCode = NewType("MinimizedSourceCode", str)
Seed = NewType("Seed", int)
def run_ruff(executable_args: list[str], code: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[*executable_args, "check", "--select=E999", "--no-cache", "-"],
capture_output=True,
text=True,
input=code,
)
def contains_bug(code: str, *, only_new_bugs: bool = False) -> bool:
"""Return True if the code triggers a parser error and False otherwise.
If `only_new_bugs` is set to `True`,
the function also runs an installed version of Ruff on the same source code,
and only returns `True` if the bug appears on the branch you have currently
checked out but *not* in the latest release.
"""
new_result = run_ruff(["cargo", "run", "--release", "--"], code)
if not only_new_bugs:
return new_result.returncode != 0
if new_result.returncode == 0:
return False
old_result = run_ruff(["ruff"], code)
return old_result.returncode == 0
@dataclass(slots=True)
class FuzzResult:
# The seed used to generate the random Python file.
# The same seed always generates the same file.
seed: Seed
# If we found a bug, this will be the minimum Python code
# required to trigger the bug. If not, it will be `None`.
maybe_bug: MinimizedSourceCode | None
def print_description(self) -> None:
"""Describe the results of fuzzing the parser with this seed."""
if self.maybe_bug:
print(colored(f"Ran fuzzer on seed {self.seed}", "red"))
print(colored("The following code triggers a bug:", "red"))
print()
print(self.maybe_bug)
print()
else:
print(colored(f"Ran fuzzer successfully on seed {self.seed}", "green"))
def fuzz_code(seed: Seed, only_new_bugs: bool) -> FuzzResult:
"""Return a `FuzzResult` instance describing the fuzzing result from this seed."""
code = generate_random_code(seed)
if contains_bug(code, only_new_bugs=only_new_bugs):
try:
new_code = minimize_repro(code, contains_bug)
except ValueError:
# `pysource_minimize.minimize()` failed to reproduce the bug.
# This could indicate that `contains_bug()` failed due to a race condition
# from running `cargo build` concurrently, so double-check that the
# original snippet does actually reproduce the bug. If so, just go with the
# original snippet; if not, report the fuzzing as successful:
maybe_bug = MinimizedSourceCode(code) if contains_bug(code) else None
else:
maybe_bug = MinimizedSourceCode(new_code)
else:
maybe_bug = None
return FuzzResult(seed, maybe_bug)
def run_fuzzer_concurrently(args: ResolvedCliArgs) -> list[FuzzResult]:
print(
f"Concurrently running the fuzzer on "
f"{len(args.seeds)} randomly generated source-code files..."
)
bugs: list[FuzzResult] = []
with concurrent.futures.ProcessPoolExecutor() as executor:
fuzz_result_futures = [
executor.submit(fuzz_code, seed, args.only_new_bugs) for seed in args.seeds
]
try:
for future in concurrent.futures.as_completed(fuzz_result_futures):
fuzz_result = future.result()
if not args.quiet:
fuzz_result.print_description()
if fuzz_result.maybe_bug:
bugs.append(fuzz_result)
except KeyboardInterrupt:
print("\nShutting down the ProcessPoolExecutor due to KeyboardInterrupt...")
print("(This might take a few seconds)")
executor.shutdown(cancel_futures=True)
raise
return bugs
def run_fuzzer_sequentially(args: ResolvedCliArgs) -> list[FuzzResult]:
print(
f"Sequentially running the fuzzer on "
f"{len(args.seeds)} randomly generated source-code files..."
)
bugs: list[FuzzResult] = []
for seed in args.seeds:
fuzz_result = fuzz_code(seed, only_new_bugs=args.only_new_bugs)
if not args.quiet:
fuzz_result.print_description()
if fuzz_result.maybe_bug:
bugs.append(fuzz_result)
return bugs
def main(args: ResolvedCliArgs) -> None:
if args.only_new_bugs:
ruff_version = (
subprocess.run(
["ruff", "--version"], text=True, capture_output=True, check=True
)
.stdout.strip()
.split(" ")[1]
)
print(
f"As you have selected `--only-new-bugs`, "
f"bugs will only be reported if they appear on your current branch "
f"but do *not* appear in `ruff=={ruff_version}`"
)
if len(args.seeds) <= 5:
bugs = run_fuzzer_sequentially(args)
else:
bugs = run_fuzzer_concurrently(args)
noun_phrase = "New bugs" if args.only_new_bugs else "Bugs"
if bugs:
print(colored(f"{noun_phrase} found in the following seeds:", "red"))
print(*sorted(bug.seed for bug in bugs))
else:
print(colored(f"No {noun_phrase.lower()} found!", "green"))
def parse_seed_argument(arg: str) -> int | range:
"""Helper for argument parsing"""
if "-" in arg:
start, end = map(int, arg.split("-"))
if end <= start:
raise argparse.ArgumentTypeError(
f"Error when parsing seed argument {arg!r}: "
f"range end must be > range start"
)
seed_range = range(start, end + 1)
range_too_long = (
f"Error when parsing seed argument {arg!r}: "
f"maximum allowed range length is 1_000_000_000"
)
try:
if len(seed_range) > 1_000_000_000:
raise argparse.ArgumentTypeError(range_too_long)
except OverflowError:
raise argparse.ArgumentTypeError(range_too_long) from None
return range(int(start), int(end) + 1)
return int(arg)
@dataclass(slots=True)
class ResolvedCliArgs:
seeds: list[Seed]
_: KW_ONLY
only_new_bugs: bool
quiet: bool
def parse_args() -> ResolvedCliArgs:
"""Parse command-line arguments"""
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument(
"seeds",
type=parse_seed_argument,
nargs="+",
help="Either a single seed, or an inclusive range of seeds in the format `0-5`",
)
parser.add_argument(
"--only-new-bugs",
action="store_true",
help=(
"Only report bugs if they exist on the current branch, "
"but *didn't* exist on the released version of Ruff "
"installed into the Python environment we're running in"
),
)
parser.add_argument(
"--quiet",
action="store_true",
help="Print fewer things to the terminal while running the fuzzer",
)
args = parser.parse_args()
seed_arguments: list[range | int] = args.seeds
seen_seeds: set[int] = set()
for arg in seed_arguments:
if isinstance(arg, int):
seen_seeds.add(arg)
else:
seen_seeds.update(arg)
return ResolvedCliArgs(
sorted(map(Seed, seen_seeds)),
only_new_bugs=args.only_new_bugs,
quiet=args.quiet,
)
if __name__ == "__main__":
args = parse_args()
main(args)

View File

@@ -0,0 +1,4 @@
pysource-codegen
pysource-minimize
ruff
termcolor

View File

@@ -0,0 +1,28 @@
# This file was autogenerated by uv via the following command:
# uv pip compile scripts/fuzz-parser/requirements.in --output-file scripts/fuzz-parser/requirements.txt
asttokens==2.4.1
# via pysource-minimize
astunparse==1.6.3
# via pysource-minimize
click==8.1.7
# via pysource-minimize
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
pygments==2.17.2
# via rich
pysource-codegen==0.5.1
pysource-minimize==0.6.2
rich==13.7.1
# via pysource-minimize
ruff==0.4.0
six==1.16.0
# via
# asttokens
# astunparse
termcolor==2.4.0
typing-extensions==4.11.0
# via pysource-codegen
wheel==0.43.0
# via astunparse

View File

@@ -11,17 +11,19 @@ line-length = 88
line-length = 88
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"C901", # McCabe complexity
"D", # pydocstyle
"PL", # pylint
"S", # bandit
"G", # flake8-logging
"T", # flake8-print
"FBT", # flake8-boolean-trap
"PERF", # perflint
"ANN401",
select = [
"E", # pycodestyle (error)
"F", # pyflakes
"B", # bugbear
"B9",
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"I", # isort
"UP", # pyupgrade
"PIE", # flake8-pie
"PGH", # pygrep-hooks
"PYI", # flake8-pyi
"RUF",
]
[tool.ruff.lint.isort]