Skip to content

Commit 0fc4e8f

Browse files
authored
Introduce InferContext (#14956)
## Summary I'm currently on the fence about landing the #14760 PR because it's unclear how we'd support tracking used and unused suppression comments in a performant way: * Salsa adds an "untracked" dependency to every query reading accumulated values. This has the effect that the query re-runs on every revision. For example, a possible future query `unused_suppression_comments(db, file)` would re-run on every incremental change and for every file. I don't expect the operation itself to be expensive, but it all adds up in a project with 100k+ files * Salsa collects the accumulated values by traversing the entire query dependency graph. It can skip over sub-graphs if it is known that they contain no accumulated values. This makes accumulators a great tool for when they are rare; diagnostics are a good example. Unfortunately, suppressions are more common, and they often appear in many different files, making the "skip over subgraphs" optimization less effective. Because of that, I want to wait to adopt salsa accumulators for type check diagnostics (we could start using them for other diagnostics) until we have very specific reasons that justify regressing incremental check performance. This PR does a "small" refactor that brings us closer to what I have in #14760 but without using accumulators. To emit a diagnostic, a method needs: * Access to the db * Access to the currently checked file This PR introduces a new `InferContext` that holds on to the db, the current file, and the reported diagnostics. It replaces the `TypeCheckDiagnosticsBuilder`. We pass the `InferContext` instead of the `db` to methods that *might* emit diagnostics. This simplifies some of the `Outcome` methods, which can now be called with a context instead of a `db` and the diagnostics builder. Having the `db` and the file on a single type like this would also be useful when using accumulators. This PR doesn't solve the issue that the `Outcome` types feel somewhat complicated nor that it can be annoying when you need to report a `Diagnostic,` but you don't have access to an `InferContext` (or the file). However, I also believe that accumulators won't solve these problems because: * Even with accumulators, it's necessary to have a reference to the file that's being checked. The struggle would be to get a reference to that file rather than getting a reference to `InferContext`. * Users of the `HasTy` trait (e.g., a linter) don't want to bother getting the `File` when calling `Type::return_ty` because they aren't interested in the created diagnostics. They just want to know what calling the current expression would return (and if it even is a callable). This is what the different methods of `Outcome` enable today. I can ask for the return type without needing extra data that's only relevant for emitting a diagnostic. A shortcoming of this approach is that it is now a bit confusing when to pass `db` and when an `InferContext`. An option is that we'd make the `file` on `InferContext` optional (it won't collect any diagnostics if `None`) and change all methods on `Type` to take `InferContext` as the first argument instead of a `db`. I'm interested in your opinion on this. Accumulators are definitely harder to use incorrectly because they remove the need to merge the diagnostics explicitly and there's no risk that we accidentally merge the diagnostics twice, resulting in duplicated diagnostics. I still value performance more over making our life slightly easier.
1 parent ac81c72 commit 0fc4e8f

File tree

10 files changed

+866
-743
lines changed

10 files changed

+866
-743
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/red_knot_python_semantic/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ bitflags = { workspace = true }
2626
camino = { workspace = true }
2727
compact_str = { workspace = true }
2828
countme = { workspace = true }
29+
drop_bomb = { workspace = true }
2930
indexmap = { workspace = true }
3031
itertools = { workspace = true }
3132
ordermap = { workspace = true }
@@ -58,4 +59,3 @@ serde = ["ruff_db/serde", "dep:serde"]
5859

5960
[lints]
6061
workspace = true
61-
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use salsa;
2+
3+
use ruff_db::{files::File, parsed::comment_ranges, source::source_text};
4+
use ruff_index::{newtype_index, IndexVec};
5+
6+
use crate::{lint::LintId, Db};
7+
8+
#[salsa::tracked(return_ref)]
9+
pub(crate) fn suppressions(db: &dyn Db, file: File) -> IndexVec<SuppressionIndex, Suppression> {
10+
let comments = comment_ranges(db.upcast(), file);
11+
let source = source_text(db.upcast(), file);
12+
13+
let mut suppressions = IndexVec::default();
14+
15+
for range in comments {
16+
let text = &source[range];
17+
18+
if text.starts_with("# type: ignore") {
19+
suppressions.push(Suppression {
20+
target: None,
21+
kind: SuppressionKind::TypeIgnore,
22+
});
23+
} else if text.starts_with("# knot: ignore") {
24+
suppressions.push(Suppression {
25+
target: None,
26+
kind: SuppressionKind::KnotIgnore,
27+
});
28+
}
29+
}
30+
31+
suppressions
32+
}
33+
34+
#[newtype_index]
35+
pub(crate) struct SuppressionIndex;
36+
37+
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
38+
pub(crate) struct Suppression {
39+
target: Option<LintId>,
40+
kind: SuppressionKind,
41+
}
42+
43+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
44+
pub(crate) enum SuppressionKind {
45+
/// A `type: ignore` comment
46+
TypeIgnore,
47+
48+
/// A `knot: ignore` comment
49+
KnotIgnore,
50+
}

crates/red_knot_python_semantic/src/types.rs

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::hash::Hash;
22

3+
use context::InferContext;
4+
use diagnostic::{report_not_iterable, report_not_iterable_possibly_unbound};
35
use indexmap::IndexSet;
46
use itertools::Itertools;
57
use ruff_db::diagnostic::Severity;
@@ -29,14 +31,15 @@ use crate::stdlib::{
2931
use crate::symbol::{Boundness, Symbol};
3032
use crate::types::call::{CallDunderResult, CallOutcome};
3133
use crate::types::class_base::ClassBase;
32-
use crate::types::diagnostic::{TypeCheckDiagnosticsBuilder, INVALID_TYPE_FORM};
34+
use crate::types::diagnostic::INVALID_TYPE_FORM;
3335
use crate::types::mro::{Mro, MroError, MroIterator};
3436
use crate::types::narrow::narrowing_constraint;
3537
use crate::{Db, FxOrderSet, Module, Program, PythonVersion};
3638

3739
mod builder;
3840
mod call;
3941
mod class_base;
42+
mod context;
4043
mod diagnostic;
4144
mod display;
4245
mod infer;
@@ -2177,17 +2180,13 @@ pub struct InvalidTypeExpressionError<'db> {
21772180
}
21782181

21792182
impl<'db> InvalidTypeExpressionError<'db> {
2180-
fn into_fallback_type(
2181-
self,
2182-
diagnostics: &mut TypeCheckDiagnosticsBuilder,
2183-
node: &ast::Expr,
2184-
) -> Type<'db> {
2183+
fn into_fallback_type(self, context: &InferContext, node: &ast::Expr) -> Type<'db> {
21852184
let InvalidTypeExpressionError {
21862185
fallback_type,
21872186
invalid_expressions,
21882187
} = self;
21892188
for error in invalid_expressions {
2190-
diagnostics.add_lint(
2189+
context.report_lint(
21912190
&INVALID_TYPE_FORM,
21922191
node.into(),
21932192
format_args!("{}", error.reason()),
@@ -2827,20 +2826,20 @@ enum IterationOutcome<'db> {
28272826
impl<'db> IterationOutcome<'db> {
28282827
fn unwrap_with_diagnostic(
28292828
self,
2829+
context: &InferContext<'db>,
28302830
iterable_node: ast::AnyNodeRef,
2831-
diagnostics: &mut TypeCheckDiagnosticsBuilder<'db>,
28322831
) -> Type<'db> {
28332832
match self {
28342833
Self::Iterable { element_ty } => element_ty,
28352834
Self::NotIterable { not_iterable_ty } => {
2836-
diagnostics.add_not_iterable(iterable_node, not_iterable_ty);
2835+
report_not_iterable(context, iterable_node, not_iterable_ty);
28372836
Type::Unknown
28382837
}
28392838
Self::PossiblyUnboundDunderIter {
28402839
iterable_ty,
28412840
element_ty,
28422841
} => {
2843-
diagnostics.add_not_iterable_possibly_unbound(iterable_node, iterable_ty);
2842+
report_not_iterable_possibly_unbound(context, iterable_node, iterable_ty);
28442843
element_ty
28452844
}
28462845
}

crates/red_knot_python_semantic/src/types/call.rs

+25-24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use super::diagnostic::{TypeCheckDiagnosticsBuilder, CALL_NON_CALLABLE};
1+
use super::context::InferContext;
2+
use super::diagnostic::CALL_NON_CALLABLE;
23
use super::{Severity, Type, TypeArrayDisplay, UnionBuilder};
34
use crate::Db;
45
use ruff_db::diagnostic::DiagnosticId;
@@ -86,24 +87,23 @@ impl<'db> CallOutcome<'db> {
8687
}
8788

8889
/// Get the return type of the call, emitting default diagnostics if needed.
89-
pub(super) fn unwrap_with_diagnostic<'a>(
90+
pub(super) fn unwrap_with_diagnostic(
9091
&self,
91-
db: &'db dyn Db,
92+
context: &InferContext<'db>,
9293
node: ast::AnyNodeRef,
93-
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
9494
) -> Type<'db> {
95-
match self.return_ty_result(db, node, diagnostics) {
95+
match self.return_ty_result(context, node) {
9696
Ok(return_ty) => return_ty,
9797
Err(NotCallableError::Type {
9898
not_callable_ty,
9999
return_ty,
100100
}) => {
101-
diagnostics.add_lint(
101+
context.report_lint(
102102
&CALL_NON_CALLABLE,
103103
node,
104104
format_args!(
105105
"Object of type `{}` is not callable",
106-
not_callable_ty.display(db)
106+
not_callable_ty.display(context.db())
107107
),
108108
);
109109
return_ty
@@ -113,13 +113,13 @@ impl<'db> CallOutcome<'db> {
113113
called_ty,
114114
return_ty,
115115
}) => {
116-
diagnostics.add_lint(
116+
context.report_lint(
117117
&CALL_NON_CALLABLE,
118118
node,
119119
format_args!(
120120
"Object of type `{}` is not callable (due to union element `{}`)",
121-
called_ty.display(db),
122-
not_callable_ty.display(db),
121+
called_ty.display(context.db()),
122+
not_callable_ty.display(context.db()),
123123
),
124124
);
125125
return_ty
@@ -129,13 +129,13 @@ impl<'db> CallOutcome<'db> {
129129
called_ty,
130130
return_ty,
131131
}) => {
132-
diagnostics.add_lint(
132+
context.report_lint(
133133
&CALL_NON_CALLABLE,
134134
node,
135135
format_args!(
136136
"Object of type `{}` is not callable (due to union elements {})",
137-
called_ty.display(db),
138-
not_callable_tys.display(db),
137+
called_ty.display(context.db()),
138+
not_callable_tys.display(context.db()),
139139
),
140140
);
141141
return_ty
@@ -144,12 +144,12 @@ impl<'db> CallOutcome<'db> {
144144
callable_ty: called_ty,
145145
return_ty,
146146
}) => {
147-
diagnostics.add_lint(
147+
context.report_lint(
148148
&CALL_NON_CALLABLE,
149149
node,
150150
format_args!(
151151
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
152-
called_ty.display(db)
152+
called_ty.display(context.db())
153153
),
154154
);
155155
return_ty
@@ -158,23 +158,22 @@ impl<'db> CallOutcome<'db> {
158158
}
159159

160160
/// Get the return type of the call as a result.
161-
pub(super) fn return_ty_result<'a>(
161+
pub(super) fn return_ty_result(
162162
&self,
163-
db: &'db dyn Db,
163+
context: &InferContext<'db>,
164164
node: ast::AnyNodeRef,
165-
diagnostics: &'a mut TypeCheckDiagnosticsBuilder<'db>,
166165
) -> Result<Type<'db>, NotCallableError<'db>> {
167166
match self {
168167
Self::Callable { return_ty } => Ok(*return_ty),
169168
Self::RevealType {
170169
return_ty,
171170
revealed_ty,
172171
} => {
173-
diagnostics.add(
172+
context.report_diagnostic(
174173
node,
175174
DiagnosticId::RevealedType,
176175
Severity::Info,
177-
format_args!("Revealed type is `{}`", revealed_ty.display(db)),
176+
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
178177
);
179178
Ok(*return_ty)
180179
}
@@ -187,14 +186,16 @@ impl<'db> CallOutcome<'db> {
187186
call_outcome,
188187
} => Err(NotCallableError::PossiblyUnboundDunderCall {
189188
callable_ty: *called_ty,
190-
return_ty: call_outcome.return_ty(db).unwrap_or(Type::Unknown),
189+
return_ty: call_outcome
190+
.return_ty(context.db())
191+
.unwrap_or(Type::Unknown),
191192
}),
192193
Self::Union {
193194
outcomes,
194195
called_ty,
195196
} => {
196197
let mut not_callable = vec![];
197-
let mut union_builder = UnionBuilder::new(db);
198+
let mut union_builder = UnionBuilder::new(context.db());
198199
let mut revealed = false;
199200
for outcome in outcomes {
200201
let return_ty = match outcome {
@@ -210,10 +211,10 @@ impl<'db> CallOutcome<'db> {
210211
*return_ty
211212
} else {
212213
revealed = true;
213-
outcome.unwrap_with_diagnostic(db, node, diagnostics)
214+
outcome.unwrap_with_diagnostic(context, node)
214215
}
215216
}
216-
_ => outcome.unwrap_with_diagnostic(db, node, diagnostics),
217+
_ => outcome.unwrap_with_diagnostic(context, node),
217218
};
218219
union_builder = union_builder.add(return_ty);
219220
}

0 commit comments

Comments
 (0)