Compare commits
18 Commits
v0.4.0
...
dhruv/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0fc84fd78 | ||
|
|
060141b1de | ||
|
|
b04cb9f92a | ||
|
|
c80b9a4a90 | ||
|
|
99f7f94538 | ||
|
|
7b3c92a979 | ||
|
|
fdbcb62adc | ||
|
|
0ff25a540c | ||
|
|
34873ec009 | ||
|
|
d3cd61f804 | ||
|
|
9b80cc09ee | ||
|
|
9bb23b0a38 | ||
|
|
06c248a126 | ||
|
|
27902b7130 | ||
|
|
97acf1d59b | ||
|
|
adf63d9013 | ||
|
|
5d3c9f2637 | ||
|
|
33529c049e |
@@ -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
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -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
7
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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
|
||||
|
||||
65
crates/ruff_linter/resources/test/fixtures/pylint/invalid_return_type_hash.py
vendored
Normal file
65
crates/ruff_linter/resources/test/fixtures/pylint/invalid_return_type_hash.py
vendored
Normal 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
|
||||
73
crates/ruff_linter/resources/test/fixtures/pylint/invalid_return_type_index.py
vendored
Normal file
73
crates/ruff_linter/resources/test/fixtures/pylint/invalid_return_type_index.py
vendored
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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!(
|
||||
|
||||
107
crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs
Normal file
107
crates/ruff_linter/src/rules/pylint/rules/invalid_hash_return.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
|
||||
|
||||
@@ -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]
|
||||
|
|
||||
@@ -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")
|
||||
|
|
||||
|
||||
@@ -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]
|
||||
|
|
||||
|
||||
@@ -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]
|
||||
|
|
||||
@@ -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);
|
||||
|
||||
@@ -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):
|
||||
|
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
match subject:
|
||||
case 1:
|
||||
case 2: ...
|
||||
@@ -1 +0,0 @@
|
||||
for d(x in y) in target: ...
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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: ...
|
||||
@@ -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: ...
|
||||
@@ -0,0 +1,3 @@
|
||||
def foo # comment
|
||||
def bar(): ...
|
||||
def baz
|
||||
@@ -1 +0,0 @@
|
||||
for d[x in y] in target: ...
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
|
||||
@@ -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
|
||||
|
|
||||
|
||||
@@ -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])
|
||||
|
|
||||
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
@@ -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
|
||||
|
|
||||
@@ -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
|
||||
|
|
||||
|
||||
@@ -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
|
||||
|
|
||||
@@ -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
|
||||
|
|
||||
@@ -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
|
||||
|
|
||||
@@ -167,7 +167,7 @@ Module(
|
||||
conversion: None,
|
||||
format_spec: Some(
|
||||
FStringFormatSpec {
|
||||
range: 43..43,
|
||||
range: 42..42,
|
||||
elements: [],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
4
ruff.schema.json
generated
@@ -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",
|
||||
|
||||
6
scripts/benchmarks/poetry.lock
generated
6
scripts/benchmarks/poetry.lock
generated
@@ -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]
|
||||
|
||||
@@ -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>"]
|
||||
|
||||
|
||||
@@ -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
244
scripts/fuzz-parser/fuzz.py
Normal 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)
|
||||
4
scripts/fuzz-parser/requirements.in
Normal file
4
scripts/fuzz-parser/requirements.in
Normal file
@@ -0,0 +1,4 @@
|
||||
pysource-codegen
|
||||
pysource-minimize
|
||||
ruff
|
||||
termcolor
|
||||
28
scripts/fuzz-parser/requirements.txt
Normal file
28
scripts/fuzz-parser/requirements.txt
Normal 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
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user