[ty] Avoid retaining transient source text
diff --git a/crates/ruff_db/src/diagnostic/render/full.rs b/crates/ruff_db/src/diagnostic/render/full.rs index 6a006a0..5dc178d 100644 --- a/crates/ruff_db/src/diagnostic/render/full.rs +++ b/crates/ruff_db/src/diagnostic/render/full.rs
@@ -553,14 +553,7 @@ .primary("example.py", "1:24", "1:24", "") .build(); - insta::assert_snapshot!(env.render(&diagnostic), @r#" - error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1a" instead - --> example.py:1:25 - | - 1 | nested_fstrings = f'␈{f'{f'␛'}'}' - | ^ - | - "#); + insta::assert_snapshot!(env.render(&diagnostic), @"error[invalid-character-sub]: Invalid unescaped character SUB, use \"\\x1a\" instead\n --> example.py:1:25\n |\n1 | nested_fstrings = f'␈{f'\u{1a}{f'␛'}'}'\n | ^\n |"); } #[test] @@ -578,14 +571,7 @@ .primary("example.py", "1:1", "1:1", "") .build(); - insta::assert_snapshot!(env.render(&diagnostic), @r#" - error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1a" instead - --> example.py:1:2 - | - 1 | ␈␛ - | ^ - | - "#); + insta::assert_snapshot!(env.render(&diagnostic), @"error[invalid-character-sub]: Invalid unescaped character SUB, use \"\\x1a\" instead\n --> example.py:1:2\n |\n1 | ␈\u{1a}␛\n | ^\n |"); Ok(()) }
diff --git a/crates/ruff_db/src/parsed.rs b/crates/ruff_db/src/parsed.rs index 227ec38..d1050d7 100644 --- a/crates/ruff_db/src/parsed.rs +++ b/crates/ruff_db/src/parsed.rs
@@ -13,7 +13,7 @@ use crate::Db; use crate::files::File; -use crate::source::source_text; +use crate::source::read_source_text; /// Returns the parsed AST of `file`, including its token stream. /// @@ -21,7 +21,7 @@ /// AST even if the file contains syntax errors. The parse errors /// are then accessible through [`Parsed::errors`]. /// -/// The query is only cached when the [`source_text()`] hasn't changed. This is because +/// The query is only cached when the file contents haven't changed. This is because /// comparing two ASTs is a non-trivial operation and every offset change is directly /// reflected in the changed AST offsets. /// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires @@ -40,7 +40,7 @@ } pub fn parsed_module_impl(db: &dyn Db, file: File) -> Parsed<ModModule> { - let source = source_text(db, file); + let source = read_source_text(db, file); let ty = file.source_type(db); let target_version = db.python_version();
diff --git a/crates/ruff_db/src/source.rs b/crates/ruff_db/src/source.rs index 5a2d1b2..2033fad 100644 --- a/crates/ruff_db/src/source.rs +++ b/crates/ruff_db/src/source.rs
@@ -16,6 +16,17 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText { let path = file.path(db); let _span = tracing::trace_span!("source_text", file = %path).entered(); + + read_source_text(db, file) +} + +/// Reads the source text without caching it as a Salsa query result. +/// +/// This still records dependencies on the file's revision and source override. +/// Use this when a query only needs the source text transiently and should not +/// retain the full source in Salsa's query cache. +pub fn read_source_text(db: &dyn Db, file: File) -> SourceText { + let path = file.path(db); let mut read_error = None; if let Some(source) = file.source_text_override(db) {
diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md index d5f4789..b284399 100644 --- a/crates/ty/docs/rules.md +++ b/crates/ty/docs/rules.md
@@ -584,7 +584,7 @@ Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> · Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> · <a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20%22escape-character-in-forward-annotation%22" target="_blank">Related issues</a> · -<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L106" target="_blank">View source</a> +<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L105" target="_blank">View source</a> </small> @@ -705,7 +705,7 @@ Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> · Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> · <a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20%22implicit-concatenated-string-type-annotation%22" target="_blank">Related issues</a> · -<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L38" target="_blank">View source</a> +<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L37" target="_blank">View source</a> </small> @@ -2220,7 +2220,7 @@ Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> · Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> · <a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20%22invalid-syntax-in-forward-annotation%22" target="_blank">Related issues</a> · -<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L63" target="_blank">View source</a> +<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L62" target="_blank">View source</a> </small> @@ -3426,7 +3426,7 @@ Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> · Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> · <a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20%22raw-string-type-annotation%22" target="_blank">Related issues</a> · -<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L13" target="_blank">View source</a> +<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fstring_annotation.rs#L12" target="_blank">View source</a> </small>
diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index 09d01a4..346fb08 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs
@@ -15,7 +15,7 @@ use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span}; use ruff_db::files::File; use ruff_db::parsed::parsed_module; -use ruff_db::source::{SourceTextError, source_text}; +use ruff_db::source::{SourceTextError, read_source_text}; use rustc_hash::FxHasher; pub use semantic_model::{ Completion, ExpectedStringLiteralCompletion, HasDefinition, HasOptionalDefinition, HasType, @@ -175,7 +175,7 @@ let mut diagnostics: Vec<Diagnostic> = Vec::new(); // Abort checking if there are IO errors. - let source = source_text(db, file); + let source = read_source_text(db, file); if let Some(read_error) = source.read_error() { return Err(IOErrorDiagnostic {
diff --git a/crates/ty_python_semantic/src/suppression.rs b/crates/ty_python_semantic/src/suppression.rs index 21dba30..7f41cb1 100644 --- a/crates/ty_python_semantic/src/suppression.rs +++ b/crates/ty_python_semantic/src/suppression.rs
@@ -8,7 +8,7 @@ use ruff_db::diagnostic::{ Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, LintName, Severity, Span, }; -use ruff_db::{files::File, parsed::parsed_module, source::source_text}; +use ruff_db::{files::File, parsed::parsed_module, source::read_source_text}; use ruff_python_ast::token::TokenKind; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -137,7 +137,7 @@ #[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)] pub(crate) fn suppressions(db: &dyn Db, file: File) -> Suppressions { let parsed = parsed_module(db, file).load(db); - let source = source_text(db, file); + let source = read_source_text(db, file); let respect_type_ignore = db.analysis_settings(file).respect_type_ignore_comments;
diff --git a/crates/ty_python_semantic/src/types/context.rs b/crates/ty_python_semantic/src/types/context.rs index 0f41eba..e40eb04 100644 --- a/crates/ty_python_semantic/src/types/context.rs +++ b/crates/ty_python_semantic/src/types/context.rs
@@ -1,8 +1,10 @@ +use std::cell::OnceCell; use std::fmt; use drop_bomb::DebugDropBomb; use ruff_db::diagnostic::DiagnosticTag; use ruff_db::parsed::ParsedModuleRef; +use ruff_db::source::{SourceText, read_source_text}; use ruff_db::{ diagnostic::{Annotation, Diagnostic, DiagnosticId, IntoDiagnosticMessage, Severity, Span}, files::File, @@ -42,6 +44,7 @@ scope: ScopeId<'db>, file: File, module: &'ast ParsedModuleRef, + source_text: OnceCell<SourceText>, diagnostics: std::cell::RefCell<TypeCheckDiagnostics>, /// This field tracks various flags that control how type inference should behave in the current context. pub(crate) inference_flags: InferenceFlags, @@ -55,6 +58,7 @@ scope, module, file: scope.file(db), + source_text: OnceCell::new(), diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()), inference_flags: InferenceFlags::empty(), bomb: DebugDropBomb::new( @@ -73,6 +77,11 @@ self.module } + pub(crate) fn source_text(&self) -> &SourceText { + self.source_text + .get_or_init(|| read_source_text(self.db, self.file)) + } + pub(crate) fn scope(&self) -> ScopeId<'db> { self.scope }
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index d58003e..1050d9b 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -33,7 +33,7 @@ use crate::types::{KnownInstanceType, MemberLookupPolicy, TypedDictType, UnionType}; use crate::{Db, DisplaySettings, FxIndexMap, Program, declare_lint}; use itertools::Itertools; -use ruff_db::source::source_text; +use ruff_db::source::read_source_text; use ruff_db::{ diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity}, parsed::parsed_module, @@ -5074,7 +5074,7 @@ let file = function.file(db); let module = parsed_module(db, file).load(db); let node = implementation.node(db, file, &module); - let source_text = source_text(db, file); + let source_text = read_source_text(db, file); if policy == AbstractMethodAnnotationPolicy::ExcludeVerboseBody && source_text.line_start(node.name.end()) != source_text.line_start(node.end())
diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 8fa5544..bc40a23 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs
@@ -7,7 +7,7 @@ use std::rc::Rc; use ruff_db::files::FilePath; -use ruff_db::source::{line_index, source_text}; +use ruff_db::source::{line_index, read_source_text}; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_literal::escape::AsciiEscape; use ruff_source_file::LineColumn; @@ -699,7 +699,7 @@ FilePath::Vendored(_) | FilePath::SystemVirtual(_) => Cow::Borrowed(path), }; let line_index = line_index(db, file); - let LineColumn { line, column } = line_index.line_column(offset, &source_text(db, file)); + let LineColumn { line, column } = line_index.line_column(offset, &read_source_text(db, file)); f.set_invalid_type_annotation(); write!(f, " @ {path}:{line}:{column}") }
diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 6942fbd..1f211f1 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs
@@ -55,7 +55,6 @@ use ruff_db::diagnostic::{Annotation, DiagnosticId, Severity, Span}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; -use ruff_db::source::source_text; use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::find_node::covering_node; use ruff_python_ast::{self as ast, OperatorPrecedence, ParameterWithDefault}; @@ -2333,7 +2332,7 @@ let value_precedence = OperatorPrecedence::from_expr(value); OperatorPrecedence::from_expr_ref(parent) >= value_precedence }); - let value_text = &source_text(db, file)[value.range()]; + let value_text = &context.source_text()[value.range()]; let replacement = if needs_parens { format!("({value_text})") } else {
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index a7a2f8e..5f84e0e 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -4,7 +4,6 @@ use itertools::Itertools; use ruff_db::files::File; use ruff_db::parsed::ParsedModuleRef; -use ruff_db::source::source_text; use ruff_python_ast::helpers::is_dotted_name; use ruff_python_ast::name::Name; use ruff_python_ast::{ @@ -9611,8 +9610,7 @@ ty = defined_type.display(self.db()) )); if is_dotted_name(value) { - let source = - &source_text(self.db(), self.file())[value.range()]; + let source = &self.context.source_text()[value.range()]; diag.help(format_args!( "This error may indicate that `{source}` was defined as \ `{source} = {special_form}` when \
diff --git a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs index 9579e30..4cf49b4 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/post_inference/static_class.rs
@@ -1,8 +1,5 @@ use itertools::Itertools; -use ruff_db::{ - diagnostic::{Annotation, SubDiagnostic, SubDiagnosticSeverity}, - source::source_text, -}; +use ruff_db::diagnostic::{Annotation, SubDiagnostic, SubDiagnosticSeverity}; use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::{self as ast, name::Name}; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -292,7 +289,7 @@ and use PEP 695 type variables", ); if let ast::Expr::Subscript(node) = node { - let source = source_text(db, context.file()); + let source = context.source_text(); let type_params_range = TextRange::new( type_params.start().saturating_add(TextSize::new(1)), type_params.end().saturating_sub(TextSize::new(1)), @@ -475,7 +472,7 @@ && let Some(index) = *generic_index && let [first_base, .., last_base] = class_node.bases() { - let source = source_text(db, context.file()); + let source = context.source_text(); let generic_base = &source[class_node.bases()[index].range()]; diagnostic.help(format_args!( "Move `{generic_base}` to the end of the bases list"
diff --git a/crates/ty_python_semantic/src/types/string_annotation.rs b/crates/ty_python_semantic/src/types/string_annotation.rs index 3a66b0d..5f86048 100644 --- a/crates/ty_python_semantic/src/types/string_annotation.rs +++ b/crates/ty_python_semantic/src/types/string_annotation.rs
@@ -1,5 +1,4 @@ use ruff_db::parsed::parsed_string_annotation; -use ruff_db::source::source_text; use ruff_python_ast::{self as ast, ModExpression, StringFlags}; use ruff_python_parser::{ParseError, ParseErrorType, Parsed}; use ruff_text_size::Ranged; @@ -129,12 +128,11 @@ string_expr: &ast::ExprStringLiteral, ) -> Option<Parsed<ModExpression>> { let file = context.file(); - let db = context.db(); let _span = tracing::trace_span!("parse_string_annotation", string=?string_expr.range(), ?file) .entered(); - let source = source_text(db, file); + let source = context.source_text(); if let Some(string_literal) = string_expr.as_single_part_string() { let prefix = string_literal.flags.prefix();