Skip to main content

ruff_db/
source.rs

1use std::borrow::Cow;
2use std::ops::Deref;
3use std::sync::Arc;
4
5use ruff_diagnostics::SourceMap;
6use ruff_notebook::Notebook;
7use ruff_python_ast::PySourceType;
8use ruff_source_file::LineIndex;
9
10use crate::Db;
11use crate::files::{File, FilePath};
12use crate::system::System;
13
14/// Reads the source text of a python text file (must be valid UTF8) or notebook.
15#[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)]
16pub fn source_text(db: &dyn Db, file: File) -> SourceText {
17    let path = file.path(db);
18    let _span = tracing::trace_span!("source_text", file = %path).entered();
19    let mut read_error = None;
20
21    if let Some(source) = file.source_text_override(db) {
22        return source.clone();
23    }
24
25    let kind = if is_notebook(db.system(), path) {
26        file.read_to_notebook(db)
27            .unwrap_or_else(|error| {
28                tracing::debug!("Failed to read notebook '{path}': {error}");
29
30                read_error = Some(SourceTextError::FailedToReadNotebook(error.to_string()));
31                Notebook::empty()
32            })
33            .into()
34    } else {
35        file.read_to_string(db)
36            .unwrap_or_else(|error| {
37                tracing::debug!("Failed to read file '{path}': {error}");
38
39                read_error = Some(SourceTextError::FailedToReadFile(error.to_string()));
40                String::new()
41            })
42            .into()
43    };
44
45    SourceText {
46        inner: Arc::new(SourceTextInner { kind, read_error }),
47    }
48}
49
50fn is_notebook(system: &dyn System, path: &FilePath) -> bool {
51    let source_type = match path {
52        FilePath::System(path) => system.source_type(path),
53        FilePath::SystemVirtual(system_virtual) => system.virtual_path_source_type(system_virtual),
54        FilePath::Vendored(_) => return false,
55    };
56
57    let with_extension_fallback =
58        source_type.or_else(|| PySourceType::try_from_extension(path.extension()?));
59
60    with_extension_fallback == Some(PySourceType::Ipynb)
61}
62
63/// The source text of a file containing python code.
64///
65/// The file containing the source text can either be a text file or a notebook.
66///
67/// Cheap cloneable in `O(1)`.
68#[derive(Clone, Eq, PartialEq, get_size2::GetSize)]
69pub struct SourceText {
70    inner: Arc<SourceTextInner>,
71}
72
73impl SourceText {
74    /// Returns the python code as a `str`.
75    pub fn as_str(&self) -> &str {
76        match &self.inner.kind {
77            SourceTextKind::Text(source) => source,
78            SourceTextKind::Notebook { notebook } => notebook.source_code(),
79        }
80    }
81
82    /// Returns the underlying notebook if this is a notebook file.
83    pub fn as_notebook(&self) -> Option<&Notebook> {
84        match &self.inner.kind {
85            SourceTextKind::Notebook { notebook } => Some(notebook),
86            SourceTextKind::Text(_) => None,
87        }
88    }
89
90    /// Returns `true` if this is a notebook source file.
91    pub fn is_notebook(&self) -> bool {
92        matches!(&self.inner.kind, SourceTextKind::Notebook { .. })
93    }
94
95    /// Returns `true` if there was an error when reading the content of the file.
96    pub fn read_error(&self) -> Option<&SourceTextError> {
97        self.inner.read_error.as_ref()
98    }
99
100    /// Returns a new instance for this file with the updated source text (Python code).
101    ///
102    /// Uses the `source_map` to preserve the cell-boundaries.
103    #[must_use]
104    pub fn with_text(&self, new_text: String, source_map: &SourceMap) -> Self {
105        let new_kind = match &self.inner.kind {
106            SourceTextKind::Text(_) => SourceTextKind::Text(new_text),
107
108            SourceTextKind::Notebook { notebook } => {
109                let mut new_notebook = notebook.as_ref().clone();
110                new_notebook.update(source_map, new_text);
111                SourceTextKind::Notebook {
112                    notebook: new_notebook.into(),
113                }
114            }
115        };
116
117        Self {
118            inner: Arc::new(SourceTextInner {
119                kind: new_kind,
120                read_error: self.inner.read_error.clone(),
121            }),
122        }
123    }
124
125    pub fn to_bytes(&self) -> Cow<'_, [u8]> {
126        match &self.inner.kind {
127            SourceTextKind::Text(source) => Cow::Borrowed(source.as_bytes()),
128            SourceTextKind::Notebook { notebook } => {
129                let mut output: Vec<u8> = Vec::new();
130                notebook
131                    .write(&mut output)
132                    .expect("writing to a Vec should never fail");
133
134                Cow::Owned(output)
135            }
136        }
137    }
138}
139
140impl Deref for SourceText {
141    type Target = str;
142
143    fn deref(&self) -> &str {
144        self.as_str()
145    }
146}
147
148impl std::fmt::Debug for SourceText {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        let mut dbg = f.debug_tuple("SourceText");
151
152        match &self.inner.kind {
153            SourceTextKind::Text(text) => {
154                dbg.field(text);
155            }
156            SourceTextKind::Notebook { notebook } => {
157                dbg.field(notebook);
158            }
159        }
160
161        dbg.finish()
162    }
163}
164
165#[derive(Eq, PartialEq, get_size2::GetSize, Clone)]
166struct SourceTextInner {
167    kind: SourceTextKind,
168    read_error: Option<SourceTextError>,
169}
170
171#[derive(Eq, PartialEq, get_size2::GetSize, Clone)]
172enum SourceTextKind {
173    Text(String),
174    Notebook {
175        // Jupyter notebooks are not very relevant for memory profiling, and contain
176        // arbitrary JSON values that do not implement the `GetSize` trait.
177        #[get_size(ignore)]
178        notebook: Box<Notebook>,
179    },
180}
181
182impl From<String> for SourceTextKind {
183    fn from(value: String) -> Self {
184        SourceTextKind::Text(value)
185    }
186}
187
188impl From<Notebook> for SourceTextKind {
189    fn from(notebook: Notebook) -> Self {
190        SourceTextKind::Notebook {
191            notebook: Box::new(notebook),
192        }
193    }
194}
195
196#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone, get_size2::GetSize)]
197pub enum SourceTextError {
198    #[error("Failed to read notebook: {0}`")]
199    FailedToReadNotebook(String),
200    #[error("Failed to read file: {0}")]
201    FailedToReadFile(String),
202}
203
204/// Computes the [`LineIndex`] for `file`.
205#[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)]
206pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
207    let _span = tracing::trace_span!("line_index", ?file).entered();
208
209    let source = source_text(db, file);
210
211    LineIndex::from_source_text(&source)
212}
213
214#[cfg(test)]
215mod tests {
216    use salsa::EventKind;
217    use salsa::Setter as _;
218
219    use ruff_source_file::OneIndexed;
220    use ruff_text_size::TextSize;
221
222    use crate::files::system_path_to_file;
223    use crate::source::{line_index, source_text};
224    use crate::system::{DbWithWritableSystem as _, SystemPath};
225    use crate::tests::TestDb;
226
227    #[test]
228    fn re_runs_query_when_file_revision_changes() -> crate::system::Result<()> {
229        let mut db = TestDb::new();
230        let path = SystemPath::new("test.py");
231
232        db.write_file(path, "x = 10")?;
233
234        let file = system_path_to_file(&db, path).unwrap();
235
236        assert_eq!(source_text(&db, file).as_str(), "x = 10");
237
238        db.write_file(path, "x = 20").unwrap();
239
240        assert_eq!(source_text(&db, file).as_str(), "x = 20");
241
242        Ok(())
243    }
244
245    #[test]
246    fn text_is_cached_if_revision_is_unchanged() -> crate::system::Result<()> {
247        let mut db = TestDb::new();
248        let path = SystemPath::new("test.py");
249
250        db.write_file(path, "x = 10")?;
251
252        let file = system_path_to_file(&db, path).unwrap();
253
254        assert_eq!(source_text(&db, file).as_str(), "x = 10");
255
256        // Change the file permission only
257        file.set_permissions(&mut db).to(Some(0o777));
258
259        db.clear_salsa_events();
260        assert_eq!(source_text(&db, file).as_str(), "x = 10");
261
262        let events = db.take_salsa_events();
263
264        assert!(
265            !events
266                .iter()
267                .any(|event| matches!(event.kind, EventKind::WillExecute { .. }))
268        );
269
270        Ok(())
271    }
272
273    #[test]
274    fn line_index_for_source() -> crate::system::Result<()> {
275        let mut db = TestDb::new();
276        let path = SystemPath::new("test.py");
277
278        db.write_file(path, "x = 10\ny = 20")?;
279
280        let file = system_path_to_file(&db, path).unwrap();
281        let index = line_index(&db, file);
282        let source = source_text(&db, file);
283
284        assert_eq!(index.line_count(), 2);
285        assert_eq!(
286            index.line_start(OneIndexed::from_zero_indexed(0), source.as_str()),
287            TextSize::new(0)
288        );
289
290        Ok(())
291    }
292
293    #[test]
294    fn notebook() -> crate::system::Result<()> {
295        let mut db = TestDb::new();
296
297        let path = SystemPath::new("test.ipynb");
298        db.write_file(
299            path,
300            r#"
301{
302    "cells": [{"cell_type": "code", "source": ["x = 10"], "metadata": {}, "outputs": []}],
303    "metadata": {
304        "kernelspec": {
305            "display_name": "Python (ruff)",
306            "language": "python",
307            "name": "ruff"
308        },
309        "language_info": {
310            "file_extension": ".py",
311            "mimetype": "text/x-python",
312            "name": "python",
313            "nbconvert_exporter": "python",
314            "pygments_lexer": "ipython3",
315            "version": "3.11.3"
316        }
317     },
318     "nbformat": 4,
319     "nbformat_minor": 4
320}"#,
321        )?;
322
323        let file = system_path_to_file(&db, path).unwrap();
324        let source = source_text(&db, file);
325
326        assert!(source.is_notebook());
327        assert_eq!(source.as_str(), "x = 10\n");
328        assert!(source.as_notebook().is_some());
329
330        Ok(())
331    }
332}