Skip to content

Commit

Permalink
Prevent use of SyntaxNodePtr and AstPtr on mutable trees
Browse files Browse the repository at this point in the history
Both of these use source code locations to identify the node, which can
get invalidated by syntax tree mutations. This commit adds assertions to
prevent their use, and adds documentation to inform users of the issue

Fixes #150
  • Loading branch information
Technohacker committed May 10, 2024
1 parent 62bd09b commit 242ed0c
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ impl<L: Language> SyntaxNode<L> {
SyntaxNode::from(self.raw.clone_for_update())
}

pub fn is_mutable(&self) -> bool {
self.raw.is_mutable()
}

pub fn detach(&self) {
self.raw.detach()
}
Expand Down
72 changes: 71 additions & 1 deletion src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ pub trait AstNode {
}

/// A "pointer" to a [`SyntaxNode`], via location in the source code.
///
/// ## Note
/// Since the location is source code dependent, this must not be used
/// with mutable syntax trees. Any changes made in such trees causes
/// the pointed node's source location to change, invalidating the pointer.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct SyntaxNodePtr<L: Language> {
kind: L::Kind,
Expand All @@ -63,7 +68,10 @@ pub struct SyntaxNodePtr<L: Language> {

impl<L: Language> SyntaxNodePtr<L> {
/// Returns a [`SyntaxNodePtr`] for the node.
///
/// Panics if the provided node is mutable
pub fn new(node: &SyntaxNode<L>) -> Self {
assert!(!node.is_mutable(), "tree is mutable");
Self { kind: node.kind(), range: node.text_range() }
}

Expand All @@ -82,10 +90,13 @@ impl<L: Language> SyntaxNodePtr<L> {
/// Also returns `None` if `root` is not actually a root (i.e. it has a
/// parent).
///
/// NOTE: If this function is called on a mutable tree, it will panic
///
/// The complexity is linear in the depth of the tree and logarithmic in
/// tree width. As most trees are shallow, thinking about this as
/// `O(log(N))` in the size of the tree is not too wrong!
pub fn try_to_node(&self, root: &SyntaxNode<L>) -> Option<SyntaxNode<L>> {
assert!(!root.is_mutable(), "tree is mutable");
if root.parent().is_some() {
return None;
}
Expand Down Expand Up @@ -113,13 +124,21 @@ impl<L: Language> SyntaxNodePtr<L> {
}

/// Like [`SyntaxNodePtr`], but remembers the type of node.
///
/// ## Note
/// As with [`SyntaxNodePtr`], this must not be used on mutable
/// syntax trees, since any mutation can cause the pointed node's
/// source location to change, invalidating the pointer
pub struct AstPtr<N: AstNode> {
raw: SyntaxNodePtr<N::Language>,
}

impl<N: AstNode> AstPtr<N> {
/// Returns an [`AstPtr`] for the node.
///
/// Panics if the provided node is mutable
pub fn new(node: &N) -> Self {
// The above mentioned panic is handled by SyntaxNodePtr
Self { raw: SyntaxNodePtr::new(node.syntax()) }
}

Expand All @@ -129,8 +148,9 @@ impl<N: AstNode> AstPtr<N> {
}

/// Given the root node containing the node `n` that `self` is a pointer to,
/// returns `n` if possible. See [`SyntaxNodePtr::try_to_node`].
/// returns `n` if possible. Panics if `root` is mutable. See [`SyntaxNodePtr::try_to_node`].
pub fn try_to_node(&self, root: &SyntaxNode<N::Language>) -> Option<N> {
// The above mentioned panic is handled by SyntaxNodePtr
N::cast(self.raw.try_to_node(root)?)
}

Expand Down Expand Up @@ -215,3 +235,53 @@ pub mod support {
parent.children_with_tokens().filter_map(|it| it.into_token()).find(|it| it.kind() == kind)
}
}

#[cfg(test)]
mod tests {
use crate::{GreenNodeBuilder, Language, SyntaxKind, SyntaxNode};

use super::SyntaxNodePtr;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct TestLanguage;
impl Language for TestLanguage {
type Kind = SyntaxKind;

fn kind_from_raw(raw: SyntaxKind) -> Self::Kind {
raw
}

fn kind_to_raw(kind: Self::Kind) -> SyntaxKind {
kind
}
}

fn build_immut_tree() -> SyntaxNode<TestLanguage> {
// Creates a single-node tree
let mut builder = GreenNodeBuilder::new();
builder.start_node(SyntaxKind(0));
builder.finish_node();

SyntaxNode::<TestLanguage>::new_root(builder.finish())
}

#[test]
#[should_panic = "tree is mutable"]
fn ensure_mut_panic_on_create() {
// Make a mutable version
let tree = build_immut_tree().clone_for_update();

SyntaxNodePtr::new(&tree);
}

#[test]
#[should_panic = "tree is mutable"]
fn ensure_mut_panic_on_deref() {
let tree = build_immut_tree();
let tree_mut = tree.clone_for_update();

// Create on immutable, convert on mutable
let syn_ptr = SyntaxNodePtr::new(&tree);
syn_ptr.to_node(&tree_mut);
}
}
4 changes: 4 additions & 0 deletions src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,10 @@ impl SyntaxNode {
SyntaxNode { ptr: NodeData::new(Some(parent), index, offset, green, mutable) }
}

pub fn is_mutable(&self) -> bool {
self.data().mutable
}

pub fn clone_for_update(&self) -> SyntaxNode {
assert!(!self.data().mutable);
match self.parent() {
Expand Down

0 comments on commit 242ed0c

Please sign in to comment.