Repository navigation
Expand file tree
/
Copy pathpredicate.rs
More file actions
433 lines (384 loc) · 16.7 KB
/
Copy pathpredicate.rs
File metadata and controls
433 lines (384 loc) · 16.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
//! _Predicates_ are Python expressions whose runtime values can affect type inference.
//!
//! We currently use predicates in two places:
//!
//! - [_Narrowing constraints_][crate::narrowing_constraints] constrain the type of
//! a binding that is visible at a particular use.
//! - [_Reachability constraints_][crate::reachability_constraints] determine the
//! static reachability of a binding, and the reachability of a statement or expression.
use crate::Program;
use ruff_db::PythonFile;
use ruff_db::files::File;
use ruff_index::{FrozenIndexVec, Idx, IndexVec};
use ruff_python_ast::{self as ast, Singleton, name::Name};
use crate::ProgramFile;
use crate::ast_ids::ExpressionNodeKey;
use crate::db::Db;
use crate::expression::Expression;
use crate::global_scope;
use crate::reachability_constraints::ScopedReachabilityConstraintId;
use crate::scope::{FileScopeId, ScopeId};
use crate::symbol::ScopedSymbolId;
// A scoped identifier for each `Predicate` in a scope.
#[derive(Clone, Debug, Copy, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)]
pub struct ScopedPredicateId(u32);
impl ScopedPredicateId {
/// A special ID that is used for an "always true" predicate.
pub(crate) const ALWAYS_TRUE: ScopedPredicateId = ScopedPredicateId(0xffff_ffff);
/// A special ID that is used for an "always false" predicate.
pub(crate) const ALWAYS_FALSE: ScopedPredicateId = ScopedPredicateId(0xffff_fffe);
const SMALLEST_TERMINAL: ScopedPredicateId = Self::ALWAYS_FALSE;
fn is_terminal(self) -> bool {
self >= Self::SMALLEST_TERMINAL
}
}
impl Idx for ScopedPredicateId {
#[inline]
fn new(value: usize) -> Self {
assert!(value <= (Self::SMALLEST_TERMINAL.0 as usize));
#[expect(clippy::cast_possible_truncation)]
Self(value as u32)
}
#[inline]
fn index(self) -> usize {
debug_assert!(!self.is_terminal());
self.0 as usize
}
}
// A collection of predicates for a given scope.
pub type Predicates<'db> = FrozenIndexVec<ScopedPredicateId, Predicate<'db>>;
#[derive(Debug, Default)]
pub(crate) struct PredicatesBuilder<'db> {
predicates: IndexVec<ScopedPredicateId, Predicate<'db>>,
}
impl<'db> PredicatesBuilder<'db> {
/// Adds a predicate. Note that we do not deduplicate predicates. If you add a `Predicate`
/// more than once, you will get distinct `ScopedPredicateId`s for each one. (This lets you
/// model predicates that might evaluate to different values at different points of execution.)
pub(crate) fn add_predicate(&mut self, predicate: Predicate<'db>) -> ScopedPredicateId {
self.predicates.push(predicate)
}
pub(crate) fn build(self) -> Predicates<'db> {
self.predicates.into()
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)]
pub struct Predicate<'db> {
pub node: PredicateNode<'db>,
pub is_positive: bool,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, get_size2::GetSize)]
pub(crate) enum PredicateOrLiteral<'db> {
Literal(bool),
Predicate(Predicate<'db>),
}
impl PredicateOrLiteral<'_> {
pub(crate) fn negated(self) -> Self {
match self {
PredicateOrLiteral::Literal(value) => PredicateOrLiteral::Literal(!value),
PredicateOrLiteral::Predicate(Predicate { node, is_positive }) => {
PredicateOrLiteral::Predicate(Predicate {
node,
is_positive: !is_positive,
})
}
}
}
}
/// A stable key for call-completion queries. Creating it while building predicates lets cached
/// queries use its ID directly, without looking up its components in Salsa's intern table again.
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct CallableAndCallExpr<'db> {
#[returns(copy)]
pub callable: Expression<'db>,
#[returns(copy)]
pub call_expr: Expression<'db>,
/// Whether the call is wrapped in an `await` expression. If `true`, `call_expr` refers to the
/// `await` expression rather than the call itself. This is used to detect terminal `await`s of
/// async functions that return `Never`.
#[returns(copy)]
pub is_await: bool,
}
// The Salsa heap is tracked separately.
impl get_size2::GetSize for CallableAndCallExpr<'_> {}
/// A call whose completion determines whether an expression statement can continue.
#[derive(Debug)]
pub struct StatementCall<'ast> {
pub call: &'ast ast::ExprCall,
pub is_await: bool,
}
impl<'ast> StatementCall<'ast> {
/// Extracts a direct or awaited call for checking whether an expression statement can return.
///
/// Callers pass the value of an expression statement, such as `sys.exit()` or `await stop()`.
/// The semantic-index builder uses the returned call to record a reachability constraint:
/// if the call returns `Never`, subsequent statements are unreachable. The
/// `redundant-condition-strict` rule uses the same extraction when recognizing defensive exits.
///
/// The `is_await` flag matters for async functions returning `Never`: calling one creates a
/// coroutine, while awaiting it prevents execution from continuing.
///
/// Calls inside larger expressions, such as `1 + sys.exit()`, are deliberately excluded.
/// Tracking their termination would require many more reachability constraints and degrade
/// analysis performance.
pub fn from_expression(expression: &'ast ast::Expr) -> Option<Self> {
match expression {
ast::Expr::Call(call) => Some(Self {
call,
is_await: false,
}),
ast::Expr::Await(ast::ExprAwait { value, .. }) => {
value.as_call_expr().map(|call| Self {
call,
is_await: true,
})
}
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)]
pub enum PredicateNode<'db> {
/// The truthiness of an expression's resulting value.
Expression(Expression<'db>),
/// A boolean operation, `not`, or conditional expression evaluated directly as a condition.
///
/// In `if x and False`, the truthy branch is unreachable. But after `y = x and False`,
/// `if y` may be truthy: it can call `x.__bool__` a second time and get a different result.
Condition(Expression<'db>),
/// A chained comparison evaluated directly as a condition. Its inferred truthiness is
/// available without walking the expression again.
ChainedComparisonCondition(Expression<'db>),
/// Whether a context manager's exit return type allows an exception to be suppressed.
///
/// Resolved during type inference because the context manager's type is unavailable during
/// semantic indexing.
ContextManagerSuppresses {
expression: Expression<'db>,
is_async: bool,
},
/// Whether semantic evaluation rules out every normal entry into a `finally` suite.
///
/// The continuation is captured before constructing this predicate, so its constraint cannot
/// depend on the predicate itself. Deferring evaluation preserves terminal cleanup paths when
/// a context manager's suppression behavior is unavailable during semantic indexing.
FinallyNormalPathImpossible {
scope: ScopeId<'db>,
continuation: ScopedReachabilityConstraintId,
},
/// These predicates are recorded for statements with call expressions. As part of
/// reachability constraints, they are used to determine whether control flow can
/// continue past this statement or not.
///
/// The predicate evaluates to
/// [`crate::Truthiness::AlwaysTrue`] in the common case where a call
/// is inferred as returning an inhabited type: in these situations, we will
/// infer control flow as flowing through the call expression without
/// terminating. If it can be statically guaranteed that a call always
/// returns `Never`/`NoReturn`, however, the predicate evaluates to
/// [`crate::Truthiness::AlwaysFalse`], signaling that control flow
/// ends as a result of the call: these call expressions are terminal.
///
/// These predicates never evaluate to
/// [`crate::Truthiness::Ambiguous`], even if the return type of the
/// call is `Unknown`/`Any`, because that would result in too many false
/// positives.
IsNonTerminalCall(CallableAndCallExpr<'db>),
/// Whether an iterable is statically known to yield at least one item.
///
/// Currently, this predicate is only emitted for direct `range(...)` calls. It is resolved
/// semantically during type checking, so calls to a shadowed `range` remain ambiguous.
IsNonEmptyIterable(Expression<'db>),
Pattern(PatternPredicate<'db>),
/// Whether control flow takes one branch of an OR pattern instead of its remaining
/// alternatives. The selected branch is unknown, but recording a predicate and its negation
/// preserves the fact that exactly one branch is taken.
OrPatternAlternative(ScopeId<'db>),
SubjectElementPattern(SubjectElementPatternPredicate<'db>),
StarImportPlaceholder(StarImportPlaceholderPredicate<'db>),
}
/// A pattern predicate applied to one expression in a sequence-display subject.
///
/// The full pattern determines the predicate's truth value, while `target` selects the subject
/// occurrence whose aligned pattern constraint should be applied to a binding.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)]
pub struct SubjectElementPatternPredicate<'db> {
pub pattern: PatternPredicate<'db>,
pub target: ExpressionNodeKey,
}
/// Structural details for sequence patterns that affect narrowing and reachability.
#[derive(Debug, Clone, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)]
pub struct SequencePatternPredicateKind<'db> {
pub patterns: Box<[PatternPredicateKind<'db>]>,
}
impl<'db> SequencePatternPredicateKind<'db> {
/// Return `true` for `case [*rest]`, the only sequence pattern with no
/// length or element constraints.
pub fn is_irrefutable(&self) -> bool {
matches!(self.patterns.as_ref(), [PatternPredicateKind::Star(_)])
}
/// Return the patterns before and after the starred element.
pub fn split_around_star(
&self,
) -> Option<(&[PatternPredicateKind<'db>], &[PatternPredicateKind<'db>])> {
let star_index = self
.patterns
.iter()
.position(|pattern| matches!(pattern, PatternPredicateKind::Star(_)))?;
let (prefix, star_and_suffix) = self.patterns.split_at(star_index);
Some((prefix, &star_and_suffix[1..]))
}
}
/// Structural details for a class pattern.
#[derive(Debug, Clone, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)]
pub struct ClassPatternPredicateKind<'db> {
pub class: Expression<'db>,
pub positional: Box<[PatternPredicateKind<'db>]>,
pub keywords: Box<[ClassPatternKeywordPredicateKind<'db>]>,
}
impl ClassPatternPredicateKind<'_> {
pub fn is_empty(&self) -> bool {
self.positional.is_empty() && self.keywords.is_empty()
}
}
#[derive(Debug, Clone, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)]
pub struct ClassPatternKeywordPredicateKind<'db> {
pub attr: Name,
pub pattern: PatternPredicateKind<'db>,
}
/// Structural details for a mapping pattern.
#[derive(Debug, Clone, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)]
pub struct MappingPatternPredicateKind<'db> {
pub entries: Box<[MappingPatternEntryPredicateKind<'db>]>,
pub rest: Option<Name>,
}
impl MappingPatternPredicateKind<'_> {
pub fn is_irrefutable(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Clone, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)]
pub struct MappingPatternEntryPredicateKind<'db> {
pub key: Expression<'db>,
pub pattern: PatternPredicateKind<'db>,
}
/// Pattern structure used for type narrowing, static reachability, and inferring the types of
/// names bound by a successful match.
#[derive(Debug, Clone, Hash, PartialEq, get_size2::GetSize, salsa::SalsaValue)]
pub enum PatternPredicateKind<'db> {
Singleton(Singleton),
Value(Expression<'db>),
Or(Box<[PatternPredicateKind<'db>]>),
Class(ClassPatternPredicateKind<'db>),
Mapping(MappingPatternPredicateKind<'db>),
Sequence(SequencePatternPredicateKind<'db>),
As(Option<Box<PatternPredicateKind<'db>>>, Option<Name>),
Star(Option<Name>),
}
#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct PatternPredicate<'db> {
#[returns(copy)]
pub program_file: ProgramFile<'db>,
#[returns(copy)]
pub file_scope: FileScopeId,
#[returns(copy)]
pub subject: Expression<'db>,
#[returns(ref)]
pub kind: PatternPredicateKind<'db>,
#[returns(copy)]
pub guard: Option<Expression<'db>>,
/// A reference to the pattern of the previous match case
#[returns(as_deref)]
pub previous_predicate: Option<Box<PatternPredicate<'db>>>,
}
// The Salsa heap is tracked separately.
impl get_size2::GetSize for PatternPredicate<'_> {}
impl<'db> PatternPredicate<'db> {
pub fn file(self, db: &'db dyn Db) -> File {
self.program_file(db).file(db)
}
pub fn python_file(self, db: &'db dyn Db) -> PythonFile<'db> {
self.program_file(db).python_file(db)
}
pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.file_scope(db).to_scope_id(db, self.program_file(db))
}
pub fn program(self, db: &'db dyn Db) -> Program<'db> {
self.scope(db).program(db)
}
}
/// A "placeholder predicate" that is used to model the fact that the boundness of a (possible)
/// definition or declaration caused by a `*` import cannot be fully determined until type-
/// inference time. This is essentially the same as a standard reachability constraint, so we reuse
/// the [`Predicate`] infrastructure to model it.
///
/// To illustrate, say we have a module `exporter.py` like so:
///
/// ```py
/// if <condition>:
/// class A: ...
/// ```
///
/// and we have a module `importer.py` like so:
///
/// ```py
/// A = 1
///
/// from exporter import *
/// ```
///
/// Since we cannot know whether or not <condition> is true at semantic-index time, we record
/// a definition for `A` in `importer.py` as a result of the `from exporter import *` statement,
/// but place a predicate on it to record the fact that we don't yet know whether this definition
/// will be visible from all control-flow paths or not. Essentially, we model `importer.py` as
/// something similar to this:
///
/// ```py
/// A = 1
///
/// if <star_import_placeholder_predicate>:
/// from a import A
/// ```
///
/// At type-check time, the placeholder predicate for the `A` definition is evaluated by attempting
/// to resolve the `A` symbol in `exporter.py`'s global namespace:
/// - If it resolves to a definitely bound symbol, then the predicate resolves to [`Truthiness::AlwaysTrue`]
/// - If it resolves to an unbound symbol, then the predicate resolves to [`Truthiness::AlwaysFalse`]
/// - If it resolves to a possibly bound symbol, then the predicate resolves to [`Truthiness::Ambiguous`]
///
/// [Truthiness]: [crate::types::Truthiness]
#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct StarImportPlaceholderPredicate<'db> {
#[returns(copy)]
pub importing_file: ProgramFile<'db>,
/// Each symbol imported by a `*` import has a separate predicate associated with it:
/// this field identifies which symbol that is.
///
/// Note that a [`ScopedPlaceId`] is only meaningful if you also know the scope
/// it is relative to. For this specific struct, however, there's no need to store a
/// separate field to hold the ID of the scope. `StarImportPredicate`s are only created
/// for valid `*`-import definitions, and valid `*`-import definitions can only ever
/// exist in the global scope; thus, we know that the `symbol_id` here will be relative
/// to the global scope of the importing file.
#[returns(copy)]
pub symbol_id: ScopedSymbolId,
#[returns(copy)]
pub referenced_file: ProgramFile<'db>,
}
// The Salsa heap is tracked separately.
impl get_size2::GetSize for StarImportPlaceholderPredicate<'_> {}
impl<'db> StarImportPlaceholderPredicate<'db> {
pub fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
// See doc-comment above [`StarImportPlaceholderPredicate::symbol_id`]:
// valid `*`-import definitions can only take place in the global scope.
global_scope(db, self.importing_file(db))
}
}
impl<'db> From<StarImportPlaceholderPredicate<'db>> for PredicateOrLiteral<'db> {
fn from(predicate: StarImportPlaceholderPredicate<'db>) -> Self {
PredicateOrLiteral::Predicate(Predicate {
node: PredicateNode::StarImportPlaceholder(predicate),
is_positive: true,
})
}
}