Compare commits
2 Commits
charlie/co
...
alex/union
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c872fe4c08 | ||
|
|
f497167798 |
@@ -68,4 +68,3 @@ When working on ty, PR titles should start with `[ty]` and be tagged with the `t
|
||||
- Always run `uvx pre-commit run -a` at the end of a task.
|
||||
- Avoid writing significant amounts of new code. This is often a sign that we're missing an existing method or mechanism that could help solve the problem. Look for existing utilities first.
|
||||
- Avoid falling back to patterns that require `panic!`, `unreachable!`, or `.unwrap()`. Instead, try to encode those constraints in the type system.
|
||||
- Prefer let chains (`if let` combined with `&&`) over nested `if let` statements to reduce indentation and improve readability.
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
class Simple:
|
||||
x=1
|
||||
x=2 # fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
x=1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class TrailingSemicolon:
|
||||
x=1
|
||||
x=2;x=3 ; # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
x=1;
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
def foo(): y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
] # fmt: skip
|
||||
|
||||
class MultiLineSemicolon:
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
]; x=2 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAfter:
|
||||
x=1
|
||||
x = ['a']\
|
||||
; y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonBefore:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAndNewline:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
|
||||
y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAndNewlineAndComment:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
# 1
|
||||
y=1 # fmt: skip
|
||||
|
||||
class RepeatedLineContinuation:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
\
|
||||
\
|
||||
y=1 # fmt: skip
|
||||
|
||||
class MultiLineSemicolonComments:
|
||||
x=1
|
||||
# 1
|
||||
x = [ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
# 4
|
||||
]; x=2 # 5 # fmt: skip # 6
|
||||
|
||||
class DocstringSkipped:
|
||||
'''This is a docstring''' # fmt: skip
|
||||
x=1
|
||||
|
||||
class MultilineDocstringSkipped:
|
||||
'''This is a docstring
|
||||
''' # fmt: skip
|
||||
x=1
|
||||
|
||||
class FirstStatementNewlines:
|
||||
|
||||
|
||||
|
||||
|
||||
x=1 # fmt: skip
|
||||
|
||||
class ChainingSemicolons:
|
||||
x=[
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
];x=1;x=[
|
||||
'1',
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # fmt: skip
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
class MixingCompound:
|
||||
def foo(): bar(); import zoo # fmt: skip
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17331
|
||||
def main() -> None:
|
||||
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11430
|
||||
print(); print() # noqa # fmt: skip
|
||||
@@ -1,22 +0,0 @@
|
||||
# 0
|
||||
'''Module docstring
|
||||
multiple lines''' # 1 # fmt: skip # 2
|
||||
|
||||
import a;import b; from c import (
|
||||
x,y,z
|
||||
); import f # fmt: skip
|
||||
|
||||
x=1;x=2;x=3;x=4 # fmt: skip
|
||||
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
def foo(): x=[
|
||||
'1',
|
||||
'2',
|
||||
];x=1 # fmt: skip
|
||||
@@ -1,68 +0,0 @@
|
||||
class Simple:
|
||||
# Range comprises skip range
|
||||
x=1
|
||||
<RANGE_START>x=2 <RANGE_END># fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
# Range is part of skip range
|
||||
x=1
|
||||
x=2;<RANGE_START>x=3<RANGE_END> # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatFirst:
|
||||
x=1
|
||||
<RANGE_START>x=2<RANGE_END>;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatMiddle:
|
||||
x=1
|
||||
x=2;<RANGE_START>x=3<RANGE_END>;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on right side
|
||||
<RANGE_START>x=1;
|
||||
x=2<RANGE_END>;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on left side
|
||||
x=1;
|
||||
x=2;<RANGE_START>x=3 # fmt: skip
|
||||
x=4<RANGE_END>
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
<RANGE_START>def foo(): y=1 <RANGE_END># fmt: skip
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
# Range inside statement
|
||||
x=1
|
||||
x = <RANGE_START>[
|
||||
'1',
|
||||
'2',<RANGE_END>
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3<RANGE_START>
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4<RANGE_END> # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
@@ -24,6 +24,7 @@ pub use crate::options::{
|
||||
};
|
||||
use crate::range::is_logical_line;
|
||||
pub use crate::shared_traits::{AsFormat, FormattedIter, FormattedIterExt, IntoFormat};
|
||||
use crate::verbatim::suppressed_node;
|
||||
|
||||
pub(crate) mod builders;
|
||||
pub mod cli;
|
||||
@@ -60,39 +61,51 @@ where
|
||||
let node_ref = AnyNodeRef::from(node);
|
||||
let node_comments = comments.leading_dangling_trailing(node_ref);
|
||||
|
||||
leading_comments(node_comments.leading).fmt(f)?;
|
||||
if self.is_suppressed(node_comments.trailing, f.context()) {
|
||||
suppressed_node(node_ref).fmt(f)
|
||||
} else {
|
||||
leading_comments(node_comments.leading).fmt(f)?;
|
||||
|
||||
// Emit source map information for nodes that are valid "narrowing" targets
|
||||
// in range formatting. Never emit source map information if they're disabled
|
||||
// for performance reasons.
|
||||
let emit_source_position = (is_logical_line(node_ref) || node_ref.is_mod_module())
|
||||
&& f.options().source_map_generation().is_enabled();
|
||||
// Emit source map information for nodes that are valid "narrowing" targets
|
||||
// in range formatting. Never emit source map information if they're disabled
|
||||
// for performance reasons.
|
||||
let emit_source_position = (is_logical_line(node_ref) || node_ref.is_mod_module())
|
||||
&& f.options().source_map_generation().is_enabled();
|
||||
|
||||
emit_source_position
|
||||
.then_some(source_position(node.start()))
|
||||
.fmt(f)?;
|
||||
emit_source_position
|
||||
.then_some(source_position(node.start()))
|
||||
.fmt(f)?;
|
||||
|
||||
self.fmt_fields(node, f)?;
|
||||
self.fmt_fields(node, f)?;
|
||||
|
||||
debug_assert!(
|
||||
node_comments
|
||||
.dangling
|
||||
.iter()
|
||||
.all(SourceComment::is_formatted),
|
||||
"The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."
|
||||
);
|
||||
debug_assert!(
|
||||
node_comments
|
||||
.dangling
|
||||
.iter()
|
||||
.all(SourceComment::is_formatted),
|
||||
"The node has dangling comments that need to be formatted manually. Add the special dangling comments handling to `fmt_fields`."
|
||||
);
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
emit_source_position.then_some(source_position(node.end())),
|
||||
trailing_comments(node_comments.trailing)
|
||||
]
|
||||
)
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
emit_source_position.then_some(source_position(node.end())),
|
||||
trailing_comments(node_comments.trailing)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats the node's fields.
|
||||
fn fmt_fields(&self, item: &N, f: &mut PyFormatter) -> FormatResult<()>;
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
_trailing_comments: &[SourceComment],
|
||||
_context: &PyFormatContext,
|
||||
) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, salsa::Update, PartialEq, Eq)]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::Decorator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::verbatim::verbatim_text;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -12,27 +11,26 @@ pub struct FormatDecorator;
|
||||
|
||||
impl FormatNodeRule<Decorator> for FormatDecorator {
|
||||
fn fmt_fields(&self, item: &Decorator, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let comments = f.context().comments();
|
||||
let trailing = comments.trailing(item);
|
||||
let Decorator {
|
||||
expression,
|
||||
range: _,
|
||||
node_index: _,
|
||||
} = item;
|
||||
|
||||
if has_skip_comment(trailing, f.context().source()) {
|
||||
comments.mark_verbatim_node_comments_formatted(item.into());
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
token("@"),
|
||||
maybe_parenthesize_expression(expression, item, Parenthesize::Optional)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
verbatim_text(item.range()).fmt(f)
|
||||
} else {
|
||||
let Decorator {
|
||||
expression,
|
||||
range: _,
|
||||
node_index: _,
|
||||
} = item;
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
token("@"),
|
||||
maybe_parenthesize_expression(expression, item, Parenthesize::Optional)
|
||||
]
|
||||
)
|
||||
}
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use crate::comments::Comments;
|
||||
use crate::context::{IndentLevel, NodeLevel};
|
||||
use crate::prelude::*;
|
||||
use crate::statement::suite::{DocstringStmt, skip_range};
|
||||
use crate::statement::suite::DocstringStmt;
|
||||
use crate::verbatim::{ends_suppression, starts_suppression};
|
||||
use crate::{FormatModuleError, PyFormatOptions, format_module_source};
|
||||
|
||||
@@ -251,29 +251,7 @@ impl<'ast> SourceOrderVisitor<'ast> for FindEnclosingNode<'_, 'ast> {
|
||||
// We only visit statements that aren't suppressed that's why we don't need to track the suppression
|
||||
// state in a stack. Assert that this assumption is safe.
|
||||
debug_assert!(self.suppressed.is_no());
|
||||
|
||||
let mut iter = body.iter();
|
||||
|
||||
while let Some(stmt) = iter.next() {
|
||||
// If the range intersects a skip range then we need to
|
||||
// format the entire suite to properly handle the case
|
||||
// where a `fmt: skip` affects multiple statements.
|
||||
//
|
||||
// For example, in the case
|
||||
//
|
||||
// ```
|
||||
// <RANGE_START>x=1<RANGE_END>;x=2 # fmt: skip
|
||||
// ```
|
||||
//
|
||||
// the statement `x=1` does not "know" that it is
|
||||
// suppressed, but the suite does.
|
||||
if let Some(verbatim_range) = skip_range(stmt, iter.as_slice(), self.context)
|
||||
&& verbatim_range.intersect(self.range).is_some()
|
||||
{
|
||||
break;
|
||||
}
|
||||
self.visit_stmt(stmt);
|
||||
}
|
||||
walk_body(self, body);
|
||||
self.suppressed = Suppressed::No;
|
||||
}
|
||||
}
|
||||
@@ -583,7 +561,7 @@ impl NarrowRange<'_> {
|
||||
}
|
||||
|
||||
pub(crate) const fn is_logical_line(node: AnyNodeRef) -> bool {
|
||||
// Make sure to update [`FormatEnclosingNode`] when changing this.
|
||||
// Make sure to update [`FormatEnclosingLine`] when changing this.
|
||||
node.is_statement()
|
||||
|| node.is_decorator()
|
||||
|| node.is_except_handler()
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtAnnAssign;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::is_splittable_expression;
|
||||
use crate::expression::parentheses::Parentheses;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::stmt_assign::{
|
||||
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
|
||||
};
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtAnnAssign;
|
||||
@@ -83,4 +84,12 @@ impl FormatNodeRule<StmtAnnAssign> for FormatStmtAnnAssign {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ use ruff_formatter::prelude::{space, token};
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtAssert;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::prelude::*;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtAssert;
|
||||
@@ -40,4 +41,12 @@ impl FormatNodeRule<StmtAssert> for FormatStmtAssert {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ use crate::expression::{
|
||||
maybe_parenthesize_expression,
|
||||
};
|
||||
use crate::other::interpolated_string::InterpolatedStringLayout;
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_parenthesize_lambda_bodies_enabled;
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::string::StringLikeExtensions;
|
||||
@@ -27,6 +26,7 @@ use crate::string::implicit::{
|
||||
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
|
||||
ImplicitConcatenatedLayout,
|
||||
};
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtAssign;
|
||||
@@ -104,6 +104,14 @@ impl FormatNodeRule<StmtAssign> for FormatStmtAssign {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a single target with the equal operator.
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtAugAssign;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::parentheses::is_expression_parenthesized;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::stmt_assign::{
|
||||
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
|
||||
has_target_own_parentheses,
|
||||
};
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::{AsFormat, FormatNodeRule};
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtAugAssign;
|
||||
@@ -61,4 +62,12 @@ impl FormatNodeRule<StmtAugAssign> for FormatStmtAugAssign {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_python_ast::StmtBreak;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtBreak;
|
||||
@@ -9,4 +10,12 @@ impl FormatNodeRule<StmtBreak> for FormatStmtBreak {
|
||||
fn fmt_fields(&self, _item: &StmtBreak, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
token("break").fmt(f)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_python_ast::StmtContinue;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtContinue;
|
||||
@@ -9,4 +10,12 @@ impl FormatNodeRule<StmtContinue> for FormatStmtContinue {
|
||||
fn fmt_fields(&self, _item: &StmtContinue, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
token("continue").fmt(f)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ use ruff_python_ast::StmtDelete;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::builders::{PyFormatterExtensions, parenthesize_if_expands};
|
||||
use crate::comments::dangling_node_comments;
|
||||
use crate::comments::{SourceComment, dangling_node_comments};
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::prelude::*;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtDelete;
|
||||
@@ -57,4 +57,12 @@ impl FormatNodeRule<StmtDelete> for FormatStmtDelete {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Expr, Operator, StmtExpr};
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtExpr;
|
||||
@@ -28,6 +30,14 @@ impl FormatNodeRule<StmtExpr> for FormatStmtExpr {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_arithmetic_like(expression: &Expr) -> bool {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::StmtGlobal;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::has_skip_comment;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -45,4 +47,12 @@ impl FormatNodeRule<StmtGlobal> for FormatStmtGlobal {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::StmtImport;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtImport;
|
||||
@@ -20,4 +21,12 @@ impl FormatNodeRule<StmtImport> for FormatStmtImport {
|
||||
});
|
||||
write!(f, [token("import"), space(), names])
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use ruff_python_ast::StmtImportFrom;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::builders::{PyFormatterExtensions, TrailingComma, parenthesize_if_expands};
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::parentheses::parenthesized;
|
||||
use crate::has_skip_comment;
|
||||
use crate::other::identifier::DotDelimitedIdentifier;
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -70,4 +72,12 @@ impl FormatNodeRule<StmtImportFrom> for FormatStmtImportFrom {
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use ruff_python_ast::StmtIpyEscapeCommand;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtIpyEscapeCommand;
|
||||
@@ -10,4 +11,12 @@ impl FormatNodeRule<StmtIpyEscapeCommand> for FormatStmtIpyEscapeCommand {
|
||||
fn fmt_fields(&self, item: &StmtIpyEscapeCommand, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
source_text_slice(item.range()).fmt(f)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use ruff_formatter::{format_args, write};
|
||||
use ruff_python_ast::StmtNonlocal;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::has_skip_comment;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -45,4 +47,12 @@ impl FormatNodeRule<StmtNonlocal> for FormatStmtNonlocal {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ruff_python_ast::StmtPass;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::comments::SourceComment;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtPass;
|
||||
@@ -9,4 +10,12 @@ impl FormatNodeRule<StmtPass> for FormatStmtPass {
|
||||
fn fmt_fields(&self, _item: &StmtPass, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
token("pass").fmt(f)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtRaise;
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::maybe_parenthesize_expression;
|
||||
use crate::expression::parentheses::Parenthesize;
|
||||
use crate::prelude::*;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtRaise;
|
||||
@@ -42,4 +43,12 @@ impl FormatNodeRule<StmtRaise> for FormatStmtRaise {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::{Expr, StmtReturn};
|
||||
|
||||
use crate::comments::SourceComment;
|
||||
use crate::expression::expr_tuple::TupleParentheses;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::stmt_assign::FormatStatementsLastExpression;
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtReturn;
|
||||
@@ -42,4 +43,12 @@ impl FormatNodeRule<StmtReturn> for FormatStmtReturn {
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::StmtTypeAlias;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::comments::SourceComment;
|
||||
use crate::statement::stmt_assign::{
|
||||
AnyAssignmentOperator, AnyBeforeOperator, FormatStatementsLastExpression,
|
||||
};
|
||||
use crate::{has_skip_comment, prelude::*};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatStmtTypeAlias;
|
||||
@@ -41,4 +42,12 @@ impl FormatNodeRule<StmtTypeAlias> for FormatStmtTypeAlias {
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
fn is_suppressed(
|
||||
&self,
|
||||
trailing_comments: &[SourceComment],
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
has_skip_comment(trailing_comments, context.source())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,11 @@ use ruff_formatter::{
|
||||
use ruff_python_ast::helpers::is_compound_statement;
|
||||
use ruff_python_ast::{self as ast, Expr, PySourceType, Stmt, Suite};
|
||||
use ruff_python_ast::{AnyNodeRef, StmtExpr};
|
||||
use ruff_python_trivia::{
|
||||
SimpleTokenKind, SimpleTokenizer, lines_after, lines_after_ignoring_end_of_line_trivia,
|
||||
lines_before,
|
||||
};
|
||||
use ruff_python_trivia::{lines_after, lines_after_ignoring_end_of_line_trivia, lines_before};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::comments::{
|
||||
Comments, LeadingDanglingTrailingComments, has_skip_comment, leading_comments,
|
||||
trailing_comments,
|
||||
Comments, LeadingDanglingTrailingComments, leading_comments, trailing_comments,
|
||||
};
|
||||
use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
|
||||
use crate::other::string_literal::StringLiteralKind;
|
||||
@@ -20,9 +16,9 @@ use crate::prelude::*;
|
||||
use crate::preview::{
|
||||
is_allow_newline_after_block_open_enabled, is_blank_line_before_decorated_class_in_stub_enabled,
|
||||
};
|
||||
use crate::statement::trailing_semicolon;
|
||||
use crate::statement::stmt_expr::FormatStmtExpr;
|
||||
use crate::verbatim::{
|
||||
write_skipped_statements, write_suppressed_statements_starting_with_leading_comment,
|
||||
suppressed_node, write_suppressed_statements_starting_with_leading_comment,
|
||||
write_suppressed_statements_starting_with_trailing_comment,
|
||||
};
|
||||
|
||||
@@ -156,21 +152,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
|
||||
let first_comments = comments.leading_dangling_trailing(first);
|
||||
|
||||
let (mut preceding, mut empty_line_after_docstring) = if let Some(verbatim_range) =
|
||||
skip_range(first.statement(), iter.as_slice(), f.context())
|
||||
{
|
||||
let preceding =
|
||||
write_skipped_statements(first.statement(), &mut iter, verbatim_range, f)?;
|
||||
|
||||
// Insert a newline after a module level docstring, but treat
|
||||
// it as a docstring otherwise. See: https://github.com/psf/black/pull/3932.
|
||||
let empty_line_after_docstring =
|
||||
matches!(self.kind, SuiteKind::TopLevel | SuiteKind::Class)
|
||||
&& DocstringStmt::try_from_statement(preceding, self.kind, f.context())
|
||||
.is_some();
|
||||
|
||||
(preceding, empty_line_after_docstring)
|
||||
} else if first_comments
|
||||
let (mut preceding, mut empty_line_after_docstring) = if first_comments
|
||||
.leading
|
||||
.iter()
|
||||
.any(|comment| comment.is_suppression_off_comment(source))
|
||||
@@ -409,10 +391,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(verbatim_range) = skip_range(following, iter.as_slice(), f.context()) {
|
||||
preceding = write_skipped_statements(following, &mut iter, verbatim_range, f)?;
|
||||
preceding_comments = comments.leading_dangling_trailing(preceding);
|
||||
} else if following_comments
|
||||
if following_comments
|
||||
.leading
|
||||
.iter()
|
||||
.any(|comment| comment.is_suppression_off_comment(source))
|
||||
@@ -861,57 +840,61 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
|
||||
let comments = f.context().comments().clone();
|
||||
let node_comments = comments.leading_dangling_trailing(self.docstring);
|
||||
|
||||
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
|
||||
let string_literal = self
|
||||
.docstring
|
||||
.as_expr_stmt()
|
||||
.unwrap()
|
||||
.value
|
||||
.as_string_literal_expr()
|
||||
.unwrap();
|
||||
if FormatStmtExpr.is_suppressed(node_comments.trailing, f.context()) {
|
||||
suppressed_node(self.docstring).fmt(f)
|
||||
} else {
|
||||
// SAFETY: Safe because `DocStringStmt` guarantees that it only ever wraps a `ExprStmt` containing a `ExprStringLiteral`.
|
||||
let string_literal = self
|
||||
.docstring
|
||||
.as_expr_stmt()
|
||||
.unwrap()
|
||||
.value
|
||||
.as_string_literal_expr()
|
||||
.unwrap();
|
||||
|
||||
// We format the expression, but the statement carries the comments
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(node_comments.leading),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(self.docstring.start())),
|
||||
string_literal
|
||||
.format()
|
||||
.with_options(StringLiteralKind::Docstring),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(self.docstring.end())),
|
||||
]
|
||||
)?;
|
||||
// We format the expression, but the statement carries the comments
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(node_comments.leading),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(self.docstring.start())),
|
||||
string_literal
|
||||
.format()
|
||||
.with_options(StringLiteralKind::Docstring),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
.then_some(source_position(self.docstring.end())),
|
||||
]
|
||||
)?;
|
||||
|
||||
if self.suite_kind == SuiteKind::Class {
|
||||
// Comments after class docstrings need a newline between the docstring and the
|
||||
// comment (https://github.com/astral-sh/ruff/issues/7948).
|
||||
// ```python
|
||||
// class ModuleBrowser:
|
||||
// """Browse module classes and functions in IDLE."""
|
||||
// # ^ Insert a newline above here
|
||||
//
|
||||
// def __init__(self, master, path, *, _htest=False, _utest=False):
|
||||
// pass
|
||||
// ```
|
||||
if let Some(own_line) = node_comments
|
||||
.trailing
|
||||
.iter()
|
||||
.find(|comment| comment.line_position().is_own_line())
|
||||
{
|
||||
if lines_before(own_line.start(), f.context().source()) < 2 {
|
||||
empty_line().fmt(f)?;
|
||||
if self.suite_kind == SuiteKind::Class {
|
||||
// Comments after class docstrings need a newline between the docstring and the
|
||||
// comment (https://github.com/astral-sh/ruff/issues/7948).
|
||||
// ```python
|
||||
// class ModuleBrowser:
|
||||
// """Browse module classes and functions in IDLE."""
|
||||
// # ^ Insert a newline above here
|
||||
//
|
||||
// def __init__(self, master, path, *, _htest=False, _utest=False):
|
||||
// pass
|
||||
// ```
|
||||
if let Some(own_line) = node_comments
|
||||
.trailing
|
||||
.iter()
|
||||
.find(|comment| comment.line_position().is_own_line())
|
||||
{
|
||||
if lines_before(own_line.start(), f.context().source()) < 2 {
|
||||
empty_line().fmt(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trailing_comments(node_comments.trailing).fmt(f)
|
||||
trailing_comments(node_comments.trailing).fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -955,58 +938,6 @@ impl Format<PyFormatContext<'_>> for SuiteChildStatement<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn skip_range(
|
||||
first: &Stmt,
|
||||
statements: &[Stmt],
|
||||
context: &PyFormatContext,
|
||||
) -> Option<TextRange> {
|
||||
let start = first.start();
|
||||
let mut last_statement = first;
|
||||
|
||||
let comments = context.comments();
|
||||
let source = context.source();
|
||||
|
||||
for statement in statements {
|
||||
if new_logical_line_between_statements(
|
||||
source,
|
||||
TextRange::new(last_statement.end(), statement.start()),
|
||||
) {
|
||||
break;
|
||||
}
|
||||
last_statement = statement;
|
||||
}
|
||||
|
||||
if has_skip_comment(comments.trailing(last_statement), source) {
|
||||
Some(TextRange::new(
|
||||
start,
|
||||
trailing_semicolon(last_statement.into(), source)
|
||||
.map_or_else(|| last_statement.end(), ruff_text_size::TextRange::end),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn new_logical_line_between_statements(source: &str, between_statement_range: TextRange) -> bool {
|
||||
let mut tokenizer = SimpleTokenizer::new(source, between_statement_range).map(|tok| tok.kind());
|
||||
|
||||
while let Some(token) = tokenizer.next() {
|
||||
match token {
|
||||
SimpleTokenKind::Continuation => {
|
||||
tokenizer.next();
|
||||
}
|
||||
SimpleTokenKind::Newline => {
|
||||
return true;
|
||||
}
|
||||
// Since we are between statements, there are
|
||||
// no non-trivia tokens, so there is no need to check
|
||||
// for these and do an early return.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_formatter::format;
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
use std::slice::Iter;
|
||||
|
||||
use itertools::PeekingNext;
|
||||
use ruff_formatter::{FormatError, write};
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::Stmt;
|
||||
@@ -452,40 +451,6 @@ fn write_suppressed_statements<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub(crate) fn write_skipped_statements<'a>(
|
||||
first_skipped: &'a Stmt,
|
||||
statements: &mut std::slice::Iter<'a, Stmt>,
|
||||
verbatim_range: TextRange,
|
||||
f: &mut PyFormatter,
|
||||
) -> FormatResult<&'a Stmt> {
|
||||
let comments = f.context().comments().clone();
|
||||
comments.mark_verbatim_node_comments_formatted(first_skipped.into());
|
||||
|
||||
let mut preceding = first_skipped;
|
||||
|
||||
while let Some(prec) = statements.peeking_next(|next| next.end() <= verbatim_range.end()) {
|
||||
comments.mark_verbatim_node_comments_formatted(prec.into());
|
||||
preceding = prec;
|
||||
}
|
||||
|
||||
let first_leading = comments.leading(first_skipped);
|
||||
let preceding_trailing = comments.trailing(preceding);
|
||||
|
||||
// Write the outer comments and format the node as verbatim
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(first_leading),
|
||||
source_position(verbatim_range.start()),
|
||||
verbatim_text(verbatim_range),
|
||||
source_position(verbatim_range.end()),
|
||||
trailing_comments(preceding_trailing)
|
||||
]
|
||||
)?;
|
||||
Ok(preceding)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum InSuppression {
|
||||
No,
|
||||
@@ -928,6 +893,65 @@ impl Format<PyFormatContext<'_>> for VerbatimText {
|
||||
}
|
||||
}
|
||||
|
||||
/// Disables formatting for `node` and instead uses the same formatting as the node has in source.
|
||||
///
|
||||
/// The `node` gets indented as any formatted node to avoid syntax errors when the indentation string changes (e.g. from 2 spaces to 4).
|
||||
/// The `node`s leading and trailing comments are formatted as usual, except if they fall into the suppressed node's range.
|
||||
#[cold]
|
||||
pub(crate) fn suppressed_node<'a, N>(node: N) -> FormatSuppressedNode<'a>
|
||||
where
|
||||
N: Into<AnyNodeRef<'a>>,
|
||||
{
|
||||
FormatSuppressedNode { node: node.into() }
|
||||
}
|
||||
|
||||
pub(crate) struct FormatSuppressedNode<'a> {
|
||||
node: AnyNodeRef<'a>,
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatSuppressedNode<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
let comments = f.context().comments().clone();
|
||||
let node_comments = comments.leading_dangling_trailing(self.node);
|
||||
|
||||
// Mark all comments as formatted that fall into the node range
|
||||
for comment in node_comments.leading {
|
||||
if comment.start() > self.node.start() {
|
||||
comment.mark_formatted();
|
||||
}
|
||||
}
|
||||
|
||||
for comment in node_comments.trailing {
|
||||
if comment.start() < self.node.end() {
|
||||
comment.mark_formatted();
|
||||
}
|
||||
}
|
||||
|
||||
// Some statements may end with a semicolon. Preserve the semicolon
|
||||
let semicolon_range = self
|
||||
.node
|
||||
.is_statement()
|
||||
.then(|| trailing_semicolon(self.node, f.context().source()))
|
||||
.flatten();
|
||||
let verbatim_range = semicolon_range.map_or(self.node.range(), |semicolon| {
|
||||
TextRange::new(self.node.start(), semicolon.end())
|
||||
});
|
||||
comments.mark_verbatim_node_comments_formatted(self.node);
|
||||
|
||||
// Write the outer comments and format the node as verbatim
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
leading_comments(node_comments.leading),
|
||||
source_position(verbatim_range.start()),
|
||||
verbatim_text(verbatim_range),
|
||||
source_position(verbatim_range.end()),
|
||||
trailing_comments(node_comments.trailing)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
pub(crate) fn write_suppressed_clause_header(
|
||||
header: ClauseHeader,
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
class Simple:
|
||||
x=1
|
||||
x=2 # fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
x=1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class TrailingSemicolon:
|
||||
x=1
|
||||
x=2;x=3 ; # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
x=1;
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
def foo(): y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
] # fmt: skip
|
||||
|
||||
class MultiLineSemicolon:
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
]; x=2 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAfter:
|
||||
x=1
|
||||
x = ['a']\
|
||||
; y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonBefore:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAndNewline:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
|
||||
y=1 # fmt: skip
|
||||
|
||||
class LineContinuationSemicolonAndNewlineAndComment:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
# 1
|
||||
y=1 # fmt: skip
|
||||
|
||||
class RepeatedLineContinuation:
|
||||
x=1
|
||||
x = ['a']; \
|
||||
\
|
||||
\
|
||||
y=1 # fmt: skip
|
||||
|
||||
class MultiLineSemicolonComments:
|
||||
x=1
|
||||
# 1
|
||||
x = [ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
# 4
|
||||
]; x=2 # 5 # fmt: skip # 6
|
||||
|
||||
class DocstringSkipped:
|
||||
'''This is a docstring''' # fmt: skip
|
||||
x=1
|
||||
|
||||
class MultilineDocstringSkipped:
|
||||
'''This is a docstring
|
||||
''' # fmt: skip
|
||||
x=1
|
||||
|
||||
class FirstStatementNewlines:
|
||||
|
||||
|
||||
|
||||
|
||||
x=1 # fmt: skip
|
||||
|
||||
class ChainingSemicolons:
|
||||
x=[
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
];x=1;x=[
|
||||
'1',
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # fmt: skip
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
class MixingCompound:
|
||||
def foo(): bar(); import zoo # fmt: skip
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17331
|
||||
def main() -> None:
|
||||
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11430
|
||||
print(); print() # noqa # fmt: skip
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
class Simple:
|
||||
x = 1
|
||||
x=2 # fmt: skip
|
||||
x = 3
|
||||
|
||||
|
||||
class Semicolon:
|
||||
x = 1
|
||||
x=2;x=3 # fmt: skip
|
||||
x = 4
|
||||
|
||||
|
||||
class TrailingSemicolon:
|
||||
x = 1
|
||||
x=2;x=3 ; # fmt: skip
|
||||
x = 4
|
||||
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
x = 1
|
||||
x=2;x=3 # fmt: skip
|
||||
x = 4
|
||||
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x = 1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x = 5
|
||||
|
||||
|
||||
class CompoundInSuite:
|
||||
x = 1
|
||||
|
||||
def foo(): y=1 # fmt: skip
|
||||
|
||||
x = 2
|
||||
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x = 1
|
||||
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
|
||||
x = 2
|
||||
|
||||
|
||||
class MultiLineSkip:
|
||||
x = 1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
class MultiLineSemicolon:
|
||||
x = 1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
]; x=2 # fmt: skip
|
||||
|
||||
|
||||
class LineContinuationSemicolonAfter:
|
||||
x = 1
|
||||
x = ['a']\
|
||||
; y=1 # fmt: skip
|
||||
|
||||
|
||||
class LineContinuationSemicolonBefore:
|
||||
x = 1
|
||||
x = ['a']; \
|
||||
y=1 # fmt: skip
|
||||
|
||||
|
||||
class LineContinuationSemicolonAndNewline:
|
||||
x = 1
|
||||
x = ["a"]
|
||||
y=1 # fmt: skip
|
||||
|
||||
|
||||
class LineContinuationSemicolonAndNewlineAndComment:
|
||||
x = 1
|
||||
x = ["a"]
|
||||
# 1
|
||||
y=1 # fmt: skip
|
||||
|
||||
|
||||
class RepeatedLineContinuation:
|
||||
x = 1
|
||||
x = ['a']; \
|
||||
\
|
||||
\
|
||||
y=1 # fmt: skip
|
||||
|
||||
|
||||
class MultiLineSemicolonComments:
|
||||
x = 1
|
||||
# 1
|
||||
x = [ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
# 4
|
||||
]; x=2 # 5 # fmt: skip # 6
|
||||
|
||||
|
||||
class DocstringSkipped:
|
||||
'''This is a docstring''' # fmt: skip
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
class MultilineDocstringSkipped:
|
||||
'''This is a docstring
|
||||
''' # fmt: skip
|
||||
|
||||
x = 1
|
||||
|
||||
|
||||
class FirstStatementNewlines:
|
||||
x=1 # fmt: skip
|
||||
|
||||
|
||||
class ChainingSemicolons:
|
||||
x=[
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
];x=1;x=[
|
||||
'1',
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # fmt: skip
|
||||
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
|
||||
class MixingCompound:
|
||||
def foo(): bar(); import zoo # fmt: skip
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/17331
|
||||
def main() -> None:
|
||||
import ipdb; ipdb.set_trace() # noqa: E402,E702,I001 # fmt: skip
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/11430
|
||||
print(); print() # noqa # fmt: skip
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
# 0
|
||||
'''Module docstring
|
||||
multiple lines''' # 1 # fmt: skip # 2
|
||||
|
||||
import a;import b; from c import (
|
||||
x,y,z
|
||||
); import f # fmt: skip
|
||||
|
||||
x=1;x=2;x=3;x=4 # fmt: skip
|
||||
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
def foo(): x=[
|
||||
'1',
|
||||
'2',
|
||||
];x=1 # fmt: skip
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
# 0
|
||||
'''Module docstring
|
||||
multiple lines''' # 1 # fmt: skip # 2
|
||||
|
||||
import a;import b; from c import (
|
||||
x,y,z
|
||||
); import f # fmt: skip
|
||||
|
||||
x=1;x=2;x=3;x=4 # fmt: skip
|
||||
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
];x=1;x=1 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
|
||||
def foo():
|
||||
x=[
|
||||
'1',
|
||||
'2',
|
||||
];x=1 # fmt: skip
|
||||
```
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
class Simple:
|
||||
# Range comprises skip range
|
||||
x=1
|
||||
<RANGE_START>x=2 <RANGE_END># fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
# Range is part of skip range
|
||||
x=1
|
||||
x=2;<RANGE_START>x=3<RANGE_END> # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatFirst:
|
||||
x=1
|
||||
<RANGE_START>x=2<RANGE_END>;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatMiddle:
|
||||
x=1
|
||||
x=2;<RANGE_START>x=3<RANGE_END>;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on right side
|
||||
<RANGE_START>x=1;
|
||||
x=2<RANGE_END>;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on left side
|
||||
x=1;
|
||||
x=2;<RANGE_START>x=3 # fmt: skip
|
||||
x=4<RANGE_END>
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
<RANGE_START>def foo(): y=1 <RANGE_END># fmt: skip
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
# Range inside statement
|
||||
x=1
|
||||
x = <RANGE_START>[
|
||||
'1',
|
||||
'2',<RANGE_END>
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3<RANGE_START>
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4<RANGE_END> # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
class Simple:
|
||||
# Range comprises skip range
|
||||
x=1
|
||||
x=2 # fmt: skip
|
||||
x=3
|
||||
|
||||
class Semicolon:
|
||||
# Range is part of skip range
|
||||
x=1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatFirst:
|
||||
x=1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class FormatMiddle:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on right side
|
||||
x = 1
|
||||
x=2;x=3 # fmt: skip
|
||||
x=4
|
||||
|
||||
class SemicolonNewLogicalLine:
|
||||
# Range overlaps on left side
|
||||
x=1;
|
||||
x=2;x=3 # fmt: skip
|
||||
x = 4
|
||||
|
||||
class ManySemicolonOneLine:
|
||||
x=1
|
||||
x=2;x=3;x=4 # fmt: skip
|
||||
x=5
|
||||
|
||||
class CompoundInSuite:
|
||||
x=1
|
||||
def foo(): y=1 # fmt: skip
|
||||
|
||||
x=2
|
||||
|
||||
class CompoundInSuiteNewline:
|
||||
x=1
|
||||
def foo():
|
||||
y=1 # fmt: skip
|
||||
x=2
|
||||
|
||||
class MultiLineSkip:
|
||||
# Range inside statement
|
||||
x=1
|
||||
x = [
|
||||
'1',
|
||||
'2',
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
class LotsOfComments:
|
||||
# 1
|
||||
x=[ # 2
|
||||
'1', # 3
|
||||
'2',
|
||||
'3'
|
||||
] ;x=2;x=3 # 4 # fmt: skip # 5
|
||||
# 6
|
||||
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
# `list[Not[str]]` is iterable
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/1880>.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from ty_extensions import Not
|
||||
|
||||
def foo(value: list[Not[str]]) -> None:
|
||||
for item in value:
|
||||
reveal_type(item) # revealed: ~str
|
||||
```
|
||||
@@ -1,33 +0,0 @@
|
||||
# Unconstrained typevars must satisfy heapq bounds
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ruff/pull/22500>.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from heapq import heappush
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
from _typeshed import SupportsRichComparison
|
||||
from ty_extensions import TypeOf, is_assignable_to, static_assert
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# `heappush` should only be assignable to callables that require a comparable typevar.
|
||||
static_assert(
|
||||
not is_assignable_to(
|
||||
TypeOf[heappush],
|
||||
Callable[[list[T], T], None],
|
||||
)
|
||||
)
|
||||
|
||||
static_assert(
|
||||
is_assignable_to(
|
||||
TypeOf[heappush],
|
||||
Callable[[list[SupportsRichComparison], SupportsRichComparison], None],
|
||||
)
|
||||
)
|
||||
```
|
||||
@@ -1,23 +0,0 @@
|
||||
# Dict operations after narrowing should still be valid
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ruff/pull/22501>.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class OrderedDict(dict[str, Any]):
|
||||
def popitem(self) -> tuple[str, Any]:
|
||||
if not self:
|
||||
raise KeyError("dictionary is empty")
|
||||
key = next(iter(self))
|
||||
value = dict.pop(self, key)
|
||||
return key, value
|
||||
|
||||
def redact(kwargs: dict[str, Any | None]) -> None:
|
||||
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
||||
if "durationMS" in kwargs:
|
||||
kwargs["durationMS"] = 1
|
||||
_ = kwargs["durationMS"]
|
||||
if "serviceId" in kwargs:
|
||||
kwargs["serviceId"] = "srv"
|
||||
```
|
||||
@@ -1833,7 +1833,7 @@ impl<'db> Type<'db> {
|
||||
///
|
||||
/// This method may have false negatives, but it should not have false positives. It should be
|
||||
/// a cheap shallow check, not an exhaustive recursive check.
|
||||
fn subtyping_is_always_reflexive(self) -> bool {
|
||||
const fn subtyping_is_always_reflexive(self) -> bool {
|
||||
match self {
|
||||
Type::Never
|
||||
| Type::FunctionLiteral(..)
|
||||
@@ -1854,6 +1854,7 @@ impl<'db> Type<'db> {
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeVar(_)
|
||||
// might inherit `Any`, but subtyping is still reflexive
|
||||
| Type::ClassLiteral(_)
|
||||
=> true,
|
||||
@@ -1865,7 +1866,6 @@ impl<'db> Type<'db> {
|
||||
| Type::Union(_)
|
||||
| Type::Intersection(_)
|
||||
| Type::Callable(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypeGuard(_)
|
||||
@@ -11724,8 +11724,8 @@ pub(super) struct MetaclassCandidate<'db> {
|
||||
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
pub struct UnionType<'db> {
|
||||
/// The union type includes values in any of these types.
|
||||
#[returns(deref)]
|
||||
pub elements: Box<[Type<'db>]>,
|
||||
#[returns(ref)]
|
||||
pub elements: FxOrderSet<Type<'db>>,
|
||||
/// Whether the value pointed to by this type is recursively defined.
|
||||
/// If `Yes`, union literal widening is performed early.
|
||||
recursively_defined: RecursivelyDefined,
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
//! shares exactly the same possible super-types, and none of them are subtypes of each other
|
||||
//! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks.
|
||||
|
||||
use std::hash::BuildHasherDefault;
|
||||
|
||||
use crate::types::enums::{enum_member_literals, enum_metadata};
|
||||
use crate::types::type_ordering::union_or_intersection_elements_ordering;
|
||||
use crate::types::{
|
||||
@@ -706,7 +708,10 @@ impl<'db> UnionBuilder<'db> {
|
||||
}
|
||||
|
||||
pub(crate) fn try_build(self) -> Option<Type<'db>> {
|
||||
let mut types = vec![];
|
||||
let mut types: FxOrderSet<Type<'db>> = FxOrderSet::with_capacity_and_hasher(
|
||||
self.elements.len(),
|
||||
BuildHasherDefault::default(),
|
||||
);
|
||||
for element in self.elements {
|
||||
match element {
|
||||
UnionElement::IntLiterals(literals) => {
|
||||
@@ -721,7 +726,9 @@ impl<'db> UnionBuilder<'db> {
|
||||
UnionElement::EnumLiterals { literals, .. } => {
|
||||
types.extend(literals.into_iter().map(Type::EnumLiteral));
|
||||
}
|
||||
UnionElement::Type(ty) => types.push(ty),
|
||||
UnionElement::Type(ty) => {
|
||||
types.insert(ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.order_elements {
|
||||
@@ -730,11 +737,14 @@ impl<'db> UnionBuilder<'db> {
|
||||
match types.len() {
|
||||
0 => None,
|
||||
1 => Some(types[0]),
|
||||
_ => Some(Type::Union(UnionType::new(
|
||||
self.db,
|
||||
types.into_boxed_slice(),
|
||||
self.recursively_defined,
|
||||
))),
|
||||
_ => {
|
||||
types.shrink_to_fit();
|
||||
Some(Type::Union(UnionType::new(
|
||||
self.db,
|
||||
types,
|
||||
self.recursively_defined,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,7 +857,7 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
db,
|
||||
enum_member_literals(db, instance.class_literal(db), None)
|
||||
.expect("Calling `enum_member_literals` on an enum class")
|
||||
.collect::<Box<[_]>>(),
|
||||
.collect::<FxOrderSet<_>>(),
|
||||
RecursivelyDefined::No,
|
||||
)),
|
||||
seen_aliases,
|
||||
@@ -1412,6 +1422,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
|
||||
|
||||
use crate::FxOrderSet;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::place::known_module_symbol;
|
||||
use crate::types::enums::enum_member_literals;
|
||||
@@ -1445,7 +1456,7 @@ mod tests {
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
|
||||
assert_eq!(union.elements(&db), &[t0, t1]);
|
||||
assert_eq!(union.elements(&db), &FxOrderSet::from_iter([t0, t1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,10 +4,10 @@ use std::fmt::Display;
|
||||
use itertools::{Either, Itertools};
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::Db;
|
||||
use crate::types::KnownClass;
|
||||
use crate::types::enums::{enum_member_literals, enum_metadata};
|
||||
use crate::types::tuple::{Tuple, TupleType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
use super::Type;
|
||||
|
||||
@@ -362,17 +362,17 @@ pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
|
||||
/// Expands a type into its possible subtypes, if applicable.
|
||||
///
|
||||
/// Returns [`None`] if the type cannot be expanded.
|
||||
fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<FxOrderSet<Type<'db>>> {
|
||||
// NOTE: Update `is_expandable_type` if this logic changes accordingly.
|
||||
match ty {
|
||||
Type::NominalInstance(instance) => {
|
||||
let class = instance.class(db);
|
||||
|
||||
if class.is_known(db, KnownClass::Bool) {
|
||||
return Some(vec![
|
||||
return Some(FxOrderSet::from_iter([
|
||||
Type::BooleanLiteral(true),
|
||||
Type::BooleanLiteral(false),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
// If the class is a fixed-length tuple subtype, we expand it to its elements.
|
||||
@@ -390,7 +390,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
})
|
||||
.multi_cartesian_product()
|
||||
.map(|types| Type::tuple(TupleType::heterogeneous(db, types)))
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<FxOrderSet<_>>();
|
||||
|
||||
if expanded.len() == 1 {
|
||||
// There are no elements in the tuple type that can be expanded.
|
||||
@@ -409,7 +409,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
|
||||
None
|
||||
}
|
||||
Type::Union(union) => Some(union.elements(db).to_vec()),
|
||||
Type::Union(union) => Some(union.elements(db).clone()),
|
||||
// We don't handle `type[A | B]` here because it's already stored in the expanded form
|
||||
// i.e., `type[A] | type[B]` which is handled by the `Type::Union` case.
|
||||
_ => None,
|
||||
@@ -418,6 +418,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::FxOrderSet;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{KnownClass, Type, UnionType};
|
||||
@@ -427,12 +428,12 @@ mod tests {
|
||||
#[test]
|
||||
fn expand_union_type() {
|
||||
let db = setup_db();
|
||||
let types = [
|
||||
let types = FxOrderSet::from_iter([
|
||||
KnownClass::Int.to_instance(&db),
|
||||
KnownClass::Str.to_instance(&db),
|
||||
KnownClass::Bytes.to_instance(&db),
|
||||
];
|
||||
let union_type = UnionType::from_elements(&db, types);
|
||||
]);
|
||||
let union_type = UnionType::from_elements(&db, &types);
|
||||
let expanded = expand_type(&db, union_type).unwrap();
|
||||
assert_eq!(expanded.len(), types.len());
|
||||
assert_eq!(expanded, types);
|
||||
@@ -443,7 +444,8 @@ mod tests {
|
||||
let db = setup_db();
|
||||
let bool_instance = KnownClass::Bool.to_instance(&db);
|
||||
let expanded = expand_type(&db, bool_instance).unwrap();
|
||||
let expected_types = [Type::BooleanLiteral(true), Type::BooleanLiteral(false)];
|
||||
let expected_types =
|
||||
FxOrderSet::from_iter([Type::BooleanLiteral(true), Type::BooleanLiteral(false)]);
|
||||
assert_eq!(expanded.len(), expected_types.len());
|
||||
assert_eq!(expanded, expected_types);
|
||||
}
|
||||
@@ -477,14 +479,14 @@ mod tests {
|
||||
UnionType::from_elements(&db, [int_ty, str_ty, bytes_ty]),
|
||||
],
|
||||
);
|
||||
let expected_types = [
|
||||
let expected_types = FxOrderSet::from_iter([
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, bytes_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, bytes_ty]),
|
||||
];
|
||||
]);
|
||||
let expanded = expand_type(&db, tuple_type2).unwrap();
|
||||
assert_eq!(expanded, expected_types);
|
||||
|
||||
@@ -498,12 +500,12 @@ mod tests {
|
||||
str_ty,
|
||||
],
|
||||
);
|
||||
let expected_types = [
|
||||
let expected_types = FxOrderSet::from_iter([
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty, str_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty, bytes_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty, str_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty, bytes_ty, str_ty]),
|
||||
];
|
||||
]);
|
||||
let expanded = expand_type(&db, tuple_type3).unwrap();
|
||||
assert_eq!(expanded, expected_types);
|
||||
|
||||
|
||||
@@ -2784,18 +2784,25 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
||||
// will allow us to error when `*args: P.args` is matched against, for example,
|
||||
// `n: int` and correctly type check when `*args: P.args` is matched against
|
||||
// `*args: P.args` (another ParamSpec).
|
||||
match union.elements(db) {
|
||||
[paramspec @ Type::TypeVar(typevar), other]
|
||||
| [other, paramspec @ Type::TypeVar(typevar)]
|
||||
if typevar.is_paramspec(db) && other.is_unknown() =>
|
||||
{
|
||||
VariadicArgumentType::ParamSpec(*paramspec)
|
||||
}
|
||||
_ => {
|
||||
// TODO: Same todo comment as in the non-paramspec case below
|
||||
VariadicArgumentType::Other(argument_type.iterate(db))
|
||||
let elements = union.elements(db);
|
||||
let mut paramspec = None;
|
||||
if elements.len() == 2 {
|
||||
match (elements[0], elements[1]) {
|
||||
(Type::TypeVar(typevar), Type::Dynamic(_))
|
||||
| (Type::Dynamic(_), Type::TypeVar(typevar))
|
||||
if typevar.is_paramspec(db) =>
|
||||
{
|
||||
paramspec = Some(Type::TypeVar(typevar));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(paramspec) = paramspec {
|
||||
VariadicArgumentType::ParamSpec(paramspec)
|
||||
} else {
|
||||
// TODO: Same todo comment as in the non-paramspec case below
|
||||
VariadicArgumentType::Other(argument_type.iterate(db))
|
||||
}
|
||||
}
|
||||
Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => {
|
||||
VariadicArgumentType::ParamSpec(paramspec)
|
||||
@@ -2915,15 +2922,20 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
||||
let value_type = match argument_type {
|
||||
Some(argument_type @ Type::Union(union)) => {
|
||||
// See the comment in `match_variadic` for why we special case this situation.
|
||||
match union.elements(db) {
|
||||
[paramspec @ Type::TypeVar(typevar), other]
|
||||
| [other, paramspec @ Type::TypeVar(typevar)]
|
||||
if typevar.is_paramspec(db) && other.is_unknown() =>
|
||||
{
|
||||
*paramspec
|
||||
let elements = union.elements(db);
|
||||
let mut paramspec = None;
|
||||
if elements.len() == 2 {
|
||||
match (elements[0], elements[1]) {
|
||||
(Type::TypeVar(typevar), Type::Dynamic(_))
|
||||
| (Type::Dynamic(_), Type::TypeVar(typevar))
|
||||
if typevar.is_paramspec(db) =>
|
||||
{
|
||||
paramspec = Some(Type::TypeVar(typevar));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => dunder_getitem_return_type(argument_type),
|
||||
}
|
||||
paramspec.unwrap_or_else(|| dunder_getitem_return_type(argument_type))
|
||||
}
|
||||
Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => paramspec,
|
||||
Some(argument_type) => dunder_getitem_return_type(argument_type),
|
||||
@@ -3572,15 +3584,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
let value_type = match argument_type {
|
||||
Type::Union(union) => {
|
||||
// See the comment in `match_variadic` for why we special case this situation.
|
||||
match union.elements(self.db) {
|
||||
[paramspec @ Type::TypeVar(typevar), other]
|
||||
| [other, paramspec @ Type::TypeVar(typevar)]
|
||||
if typevar.is_paramspec(self.db) && other.is_unknown() =>
|
||||
{
|
||||
Some(*paramspec)
|
||||
let elements = union.elements(self.db);
|
||||
let mut paramspec = None;
|
||||
if elements.len() == 2 {
|
||||
match (elements[0], elements[1]) {
|
||||
(Type::TypeVar(typevar), Type::Dynamic(_))
|
||||
| (Type::Dynamic(_), Type::TypeVar(typevar))
|
||||
if typevar.is_paramspec(self.db) =>
|
||||
{
|
||||
paramspec = Some(Type::TypeVar(typevar));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => value_type_fallback(argument_type),
|
||||
}
|
||||
paramspec.or_else(|| value_type_fallback(argument_type))
|
||||
}
|
||||
Type::TypeVar(typevar) if typevar.is_paramspec(self.db) => Some(argument_type),
|
||||
_ => value_type_fallback(argument_type),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fmt::Display;
|
||||
|
||||
@@ -1538,42 +1538,6 @@ pub(crate) struct SpecializationBuilder<'db> {
|
||||
pub(crate) type TypeVarAssignment<'db> = (BoundTypeVarIdentity<'db>, TypeVarVariance, Type<'db>);
|
||||
|
||||
impl<'db> SpecializationBuilder<'db> {
|
||||
fn contains_negated_type(&self, ty: Type<'db>) -> bool {
|
||||
struct Visitor<'db> {
|
||||
found: Cell<bool>,
|
||||
recursion_guard: TypeCollector<'db>,
|
||||
}
|
||||
|
||||
impl<'db> TypeVisitor<'db> for Visitor<'db> {
|
||||
fn should_visit_lazy_type_attributes(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn visit_type(&self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
if self.found.get() {
|
||||
return;
|
||||
}
|
||||
if let Type::Intersection(intersection) = ty {
|
||||
let has_non_truthiness_negation = intersection.negative(db).iter().any(|neg| {
|
||||
!matches!(neg, Type::AlwaysFalsy | Type::AlwaysTruthy) && !neg.is_none(db)
|
||||
});
|
||||
if has_non_truthiness_negation {
|
||||
self.found.set(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
walk_type_with_recursion_guard(db, ty, self, &self.recursion_guard);
|
||||
}
|
||||
}
|
||||
|
||||
let visitor = Visitor {
|
||||
found: Cell::new(false),
|
||||
recursion_guard: TypeCollector::default(),
|
||||
};
|
||||
visitor.visit_type(self.db, ty);
|
||||
visitor.found.get()
|
||||
}
|
||||
|
||||
pub(crate) fn new(db: &'db dyn Db, inferable: InferableTypeVars<'db, 'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
@@ -1914,14 +1878,10 @@ impl<'db> SpecializationBuilder<'db> {
|
||||
{
|
||||
match bound_typevar.typevar(self.db).bound_or_constraints(self.db) {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
let constraints = if self.contains_negated_type(ty)
|
||||
|| self.contains_negated_type(bound)
|
||||
if !ty
|
||||
.when_assignable_to(self.db, bound, self.inferable)
|
||||
.is_always_satisfied(self.db)
|
||||
{
|
||||
ty.when_constraint_set_assignable_to(self.db, bound, self.inferable)
|
||||
} else {
|
||||
ty.when_assignable_to(self.db, bound, self.inferable)
|
||||
};
|
||||
if !constraints.satisfied_by_all_typevars(self.db, self.inferable) {
|
||||
return Err(SpecializationError::MismatchedBound {
|
||||
bound_typevar,
|
||||
argument: ty,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::FxIndexSet;
|
||||
use crate::FxOrderSet;
|
||||
use crate::place::builtins_module_scope;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::definition::DefinitionKind;
|
||||
@@ -228,8 +229,8 @@ pub fn definitions_for_attribute<'db>(
|
||||
};
|
||||
|
||||
let tys = match lhs_ty {
|
||||
Type::Union(union) => union.elements(model.db()).to_vec(),
|
||||
_ => vec![lhs_ty],
|
||||
Type::Union(union) => union.elements(model.db()).clone(),
|
||||
_ => FxOrderSet::from_iter([lhs_ty]),
|
||||
};
|
||||
|
||||
// Expand intersections for each subtype into their components
|
||||
|
||||
@@ -7268,15 +7268,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.any(|overload| overload.signature.generic_context.is_some());
|
||||
|
||||
// If the type context is a union, attempt to narrow to a specific element.
|
||||
let narrow_targets: &[_] = match call_expression_tcx.annotation {
|
||||
let narrow_targets = match call_expression_tcx.annotation {
|
||||
// TODO: We could theoretically attempt to narrow to every element of
|
||||
// the power set of this union. However, this leads to an exponential
|
||||
// explosion of inference attempts, and is rarely needed in practice.
|
||||
//
|
||||
// We only need to attempt narrowing on generic calls, otherwise the type
|
||||
// context has no effect.
|
||||
Some(Type::Union(union)) if has_generic_context => union.elements(db),
|
||||
_ => &[],
|
||||
Some(Type::Union(union)) if has_generic_context => {
|
||||
Either::Left(union.elements(db).iter().copied())
|
||||
}
|
||||
_ => Either::Right(std::iter::empty()),
|
||||
};
|
||||
|
||||
// We silence diagnostics until we successfully narrow to a specific type.
|
||||
@@ -7346,10 +7348,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
// Prefer the declared type of generic classes.
|
||||
for narrowed_ty in narrow_targets
|
||||
.iter()
|
||||
.clone()
|
||||
.filter(|ty| ty.class_specialization(db).is_some())
|
||||
{
|
||||
if let Some(result) = try_narrow(*narrowed_ty) {
|
||||
if let Some(result) = try_narrow(narrowed_ty) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -7358,11 +7360,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
//
|
||||
// TODO: We could also attempt an inference without type context, but this
|
||||
// leads to similar performance issues.
|
||||
for narrowed_ty in narrow_targets
|
||||
.iter()
|
||||
.filter(|ty| ty.class_specialization(db).is_none())
|
||||
{
|
||||
if let Some(result) = try_narrow(*narrowed_ty) {
|
||||
for narrowed_ty in narrow_targets.filter(|ty| ty.class_specialization(db).is_none()) {
|
||||
if let Some(result) = try_narrow(narrowed_ty) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,23 +467,24 @@ impl<'db> Type<'db> {
|
||||
//
|
||||
// However, there is one exception to this general rule: for any given typevar `T`,
|
||||
// `T` will always be a subtype of any union containing `T`.
|
||||
(Type::TypeVar(bound_typevar), Type::Union(union))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
(_, Type::Union(union))
|
||||
if (!relation.is_subtyping() || self.subtyping_is_always_reflexive())
|
||||
&& union.elements(db).contains(&self) =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
|
||||
// A similar rule applies in reverse to intersection types.
|
||||
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
(Type::Intersection(intersection), _)
|
||||
if (!relation.is_subtyping() || target.subtyping_is_always_reflexive())
|
||||
&& intersection.positive(db).contains(&target) =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
&& intersection.negative(db).contains(&target) =>
|
||||
(Type::Intersection(intersection), _)
|
||||
if (!relation.is_subtyping() || target.subtyping_is_always_reflexive())
|
||||
&& intersection.negative(db).contains(&target)
|
||||
&& !intersection.positive(db).iter().any(Type::is_dynamic) =>
|
||||
{
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
@@ -1785,7 +1785,7 @@ impl<'db> Tuple<Type<'db>> {
|
||||
|
||||
// TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there)
|
||||
let release_level_ty = {
|
||||
let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"]
|
||||
let elements: FxOrderSet<_> = ["alpha", "beta", "candidate", "final"]
|
||||
.iter()
|
||||
.map(|level| Type::string_literal(db, level))
|
||||
.collect();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Either;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -124,11 +125,13 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
|
||||
// See <https://github.com/astral-sh/ruff/pull/20377#issuecomment-3401380305>
|
||||
// for more discussion.
|
||||
let unpack_types = match value_ty {
|
||||
Type::Union(union_ty) => union_ty.elements(self.db()),
|
||||
_ => std::slice::from_ref(&value_ty),
|
||||
Type::Union(union_ty) => {
|
||||
Either::Left(union_ty.elements(self.db()).iter().copied())
|
||||
}
|
||||
_ => Either::Right(std::iter::once(value_ty)),
|
||||
};
|
||||
|
||||
for ty in unpack_types.iter().copied() {
|
||||
for ty in unpack_types {
|
||||
let tuple = ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(&self.context, ty, value_expr);
|
||||
Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(self.db())))
|
||||
|
||||
@@ -265,29 +265,26 @@ Instead, apply the `# fmt: off` comment to the entire statement:
|
||||
Like Black, Ruff will _also_ recognize [YAPF](https://github.com/google/yapf)'s `# yapf: disable` and `# yapf: enable` pragma
|
||||
comments, which are treated equivalently to `# fmt: off` and `# fmt: on`, respectively.
|
||||
|
||||
`# fmt: skip` comments suppress formatting for a case header, decorator,
|
||||
function definition, class definition, or the preceding statements
|
||||
on the same logical line. The formatter leaves the following unchanged:
|
||||
`# fmt: skip` comments suppress formatting for a preceding statement, case header, decorator,
|
||||
function definition, or class definition:
|
||||
|
||||
```python
|
||||
if True:
|
||||
pass
|
||||
elif False: # fmt: skip
|
||||
elif False: # fmt: skip
|
||||
pass
|
||||
|
||||
@Test
|
||||
@Test2(a,b) # fmt: skip
|
||||
@Test2 # fmt: skip
|
||||
def test(): ...
|
||||
|
||||
a = [1,2,3,4,5] # fmt: skip
|
||||
a = [1, 2, 3, 4, 5] # fmt: skip
|
||||
|
||||
def test(a,b,c,d,e,f) -> int: # fmt: skip
|
||||
def test(a, b, c, d, e, f) -> int: # fmt: skip
|
||||
pass
|
||||
|
||||
x=1;x=2;x=3 # fmt: skip
|
||||
```
|
||||
|
||||
Adding a `# fmt: skip` comment at the end of an expression will have no effect. In
|
||||
As such, adding an `# fmt: skip` comment at the end of an expression will have no effect. In
|
||||
the following example, the list entry `'1'` will be formatted, despite the `# fmt: skip`:
|
||||
|
||||
```python
|
||||
|
||||
Reference in New Issue
Block a user