Skip to main content

ty_python_core/
ast_node_ref.rs

1use std::fmt::Debug;
2use std::marker::PhantomData;
3
4#[cfg(debug_assertions)]
5use ruff_db::files::File;
6use ruff_db::parsed::ParsedModuleRef;
7#[cfg(debug_assertions)]
8use ruff_python_ast::PythonVersion;
9use ruff_python_ast::{AnyNodeRef, NodeIndex};
10use ruff_python_ast::{AnyRootNodeRef, HasNodeIndex};
11use ruff_text_size::Ranged;
12
13/// Reference to an AST node.
14///
15/// This type acts as a reference to an AST node within a given module that remains
16/// stable regardless of whether the AST is garbage collected. As such, accessing a
17/// node through the [`AstNodeRef`] requires a reference to the current [`ParsedModuleRef`]
18/// for the module containing the node.
19///
20/// ## Usage in salsa tracked structs
21/// It's important that [`AstNodeRef`] fields in salsa tracked structs are tracked fields
22/// (attributed with `#[tracked`]). It prevents that the tracked struct gets a new ID
23/// every time the AST changes, which in turn, invalidates the result of any query
24/// that takes said tracked struct as a query argument or returns the tracked struct as part of its result.
25///
26/// For example, marking the [`AstNodeRef`] as tracked on `Expression`
27/// has the effect that salsa will consider the expression as "unchanged" for as long as it:
28///
29/// * belongs to the same file
30/// * belongs to the same scope
31/// * has the same kind
32/// * was created in the same order
33///
34/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving
35/// us some form of scope-stable identity for expressions. Only queries accessing the node field
36/// run on every AST change. All other queries only run when the expression's identity changes.
37#[derive(Clone)]
38pub struct AstNodeRef<T> {
39    /// The index of the node in the AST.
40    index: NodeIndex,
41
42    /// Debug information.
43    #[cfg(debug_assertions)]
44    kind: ruff_python_ast::NodeKind,
45    #[cfg(debug_assertions)]
46    range: ruff_text_size::TextRange,
47    // Note that because the module address is not stored in release builds, `AstNodeRef`
48    // cannot implement `Eq`, as indices are only unique within a given instance of the
49    // AST.
50    #[cfg(debug_assertions)]
51    file: File,
52    #[cfg(debug_assertions)]
53    python_version: PythonVersion,
54
55    _node: PhantomData<T>,
56}
57
58impl<T> AstNodeRef<T> {
59    pub fn index(&self) -> NodeIndex {
60        self.index
61    }
62}
63
64impl<T> AstNodeRef<T>
65where
66    T: HasNodeIndex + Ranged + PartialEq + Debug,
67    for<'ast> AnyNodeRef<'ast>: From<&'ast T>,
68    for<'ast> &'ast T: TryFrom<AnyRootNodeRef<'ast>>,
69{
70    /// Creates a new `AstNodeRef` that references `node`.
71    ///
72    /// This method may panic or produce unspecified results if the provided module is from a
73    /// different file, Python version, or Salsa revision than the module to which the node belongs.
74    pub(super) fn new(module_ref: &ParsedModuleRef, node: &T) -> Self {
75        let index = node.node_index().load();
76        debug_assert_eq!(module_ref.get_by_index(index).try_into().ok(), Some(node));
77
78        Self {
79            index,
80            #[cfg(debug_assertions)]
81            file: module_ref.module().file(),
82            #[cfg(debug_assertions)]
83            python_version: module_ref.module().python_version(),
84            #[cfg(debug_assertions)]
85            kind: AnyNodeRef::from(node).kind(),
86            #[cfg(debug_assertions)]
87            range: node.range(),
88            _node: PhantomData,
89        }
90    }
91
92    /// Returns a reference to the wrapped node.
93    ///
94    /// This method may panic or produce unspecified results if the provided module is from a
95    /// different file, Python version, or Salsa revision than the module to which the node belongs.
96    #[track_caller]
97    pub fn node<'ast>(&self, module_ref: &'ast ParsedModuleRef) -> &'ast T {
98        #[cfg(debug_assertions)]
99        assert_eq!(
100            (
101                module_ref.module().file(),
102                module_ref.module().python_version()
103            ),
104            (self.file, self.python_version),
105            "an `AstNodeRef` cannot be used with a module parsed for a different file or Python version"
106        );
107        // The user guarantees that the module is from the same file, Python version, and Salsa
108        // revision, so the file contents cannot have changed.
109        module_ref
110            .get_by_index(self.index)
111            .try_into()
112            .ok()
113            .expect("AST indices should never change within the same revision")
114    }
115}
116
117impl<T> get_size2::GetSize for AstNodeRef<T> {}
118
119#[expect(clippy::missing_fields_in_debug)]
120impl<T> Debug for AstNodeRef<T>
121where
122    T: Debug,
123    for<'ast> &'ast T: TryFrom<AnyRootNodeRef<'ast>>,
124{
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        cfg_select! {
127            debug_assertions => {
128                f.debug_struct("AstNodeRef")
129                    .field("kind", &self.kind)
130                    .field("range", &self.range)
131                    .finish()
132            },
133            _ => {
134                // Unfortunately we have no access to the AST here.
135                f.debug_tuple("AstNodeRef").finish_non_exhaustive()
136            },
137        }
138    }
139}
140
141#[cfg(all(test, debug_assertions))]
142mod tests {
143    use ruff_db::PythonFile;
144    use ruff_db::files::system_path_to_file;
145    use ruff_db::parsed::parsed_module;
146    use ruff_python_ast::PythonVersion;
147
148    use crate::ast_node_ref::AstNodeRef;
149    use crate::db::tests::TestDbBuilder;
150
151    #[test]
152    #[should_panic(
153        expected = "an `AstNodeRef` cannot be used with a module parsed for a different file or Python version"
154    )]
155    fn rejects_module_parsed_for_different_python_version() {
156        let db = TestDbBuilder::new()
157            .with_file("test.py", "x = 1")
158            .build()
159            .unwrap();
160        let file = system_path_to_file(&db, "test.py").unwrap();
161
162        let parsed_py311 =
163            parsed_module(&db, PythonFile::new(&db, file, PythonVersion::PY311)).load(&db);
164        let parsed_py312 =
165            parsed_module(&db, PythonFile::new(&db, file, PythonVersion::PY312)).load(&db);
166        let assignment = parsed_py311.syntax().body[0].as_assign_stmt().unwrap();
167
168        let node = AstNodeRef::new(&parsed_py311, assignment);
169        node.node(&parsed_py312);
170    }
171}