Fix to cache correct PathToNode in artifact graph (#6632)

* Add NodePath to artifact graph

Since this is cached, this should make PathToNode computation correct
even when code is formatted, whitespace changes, and source ranges
are different.

* Remove dead code

* Add unit tests

* Add tests for PathToNode conversion

* Remove unused parameter

* Add missing PathToNode cases

* Fix to handle unlabeled arg

* Cherry pick unlabeled arg fix

* Change PathToNode comment to match TS implementation
This commit is contained in:
Jonathan Tran
2025-05-01 23:55:12 -04:00
committed by GitHub
parent 02a37e207f
commit 819ee23565
12 changed files with 767 additions and 67 deletions

View File

@ -11,6 +11,7 @@ use std::{
use anyhow::Result;
use parse_display::{Display, FromStr};
pub use path::NodePath;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::{
@ -35,6 +36,7 @@ use crate::{
mod condition;
mod literal_value;
mod none;
mod path;
#[derive(Debug)]
pub enum Definition<'a> {
@ -159,6 +161,10 @@ impl<T> Node<T> {
self.start <= pos && pos <= self.end
}
pub(crate) fn contains_range(&self, range: &SourceRange) -> bool {
self.as_source_range().contains_range(range)
}
pub fn map<U>(self, f: impl Fn(T) -> U) -> Node<U> {
Node {
inner: f(self.inner),
@ -818,6 +824,11 @@ impl BodyItem {
}
}
pub(crate) fn contains_range(&self, range: &SourceRange) -> bool {
let item_range = SourceRange::from(self);
item_range.contains_range(range)
}
pub(crate) fn set_attrs(&mut self, attr: NodeList<Annotation>) {
match self {
BodyItem::ImportStatement(node) => node.outer_attrs = attr,
@ -1045,6 +1056,11 @@ impl Expr {
}
}
fn contains_range(&self, range: &SourceRange) -> bool {
let expr_range = SourceRange::from(self);
expr_range.contains_range(range)
}
/// Rename all identifiers that have the old name to the new given name.
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
match self {
@ -1162,6 +1178,21 @@ impl From<&Expr> for SourceRange {
}
}
impl From<&BinaryPart> for Expr {
fn from(value: &BinaryPart) -> Self {
match value {
BinaryPart::Literal(literal) => Expr::Literal(literal.clone()),
BinaryPart::Name(name) => Expr::Name(name.clone()),
BinaryPart::BinaryExpression(binary_expression) => Expr::BinaryExpression(binary_expression.clone()),
BinaryPart::CallExpression(call_expression) => Expr::CallExpression(call_expression.clone()),
BinaryPart::CallExpressionKw(call_expression) => Expr::CallExpressionKw(call_expression.clone()),
BinaryPart::UnaryExpression(unary_expression) => Expr::UnaryExpression(unary_expression.clone()),
BinaryPart::MemberExpression(member_expression) => Expr::MemberExpression(member_expression.clone()),
BinaryPart::IfExpression(e) => Expr::IfExpression(e.clone()),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
@ -2795,6 +2826,11 @@ impl MemberObject {
MemberObject::Identifier(identifier) => identifier.end,
}
}
pub(crate) fn contains_range(&self, range: &SourceRange) -> bool {
let sr = SourceRange::from(self);
sr.contains_range(range)
}
}
impl From<MemberObject> for SourceRange {
@ -2831,6 +2867,11 @@ impl LiteralIdentifier {
LiteralIdentifier::Literal(literal) => literal.end,
}
}
pub(crate) fn contains_range(&self, range: &SourceRange) -> bool {
let sr = SourceRange::from(self);
sr.contains_range(range)
}
}
impl From<LiteralIdentifier> for SourceRange {
@ -3349,6 +3390,11 @@ impl Parameter {
pub fn optional(&self) -> bool {
self.default_value.is_some()
}
pub(crate) fn contains_range(&self, range: &SourceRange) -> bool {
let sr = SourceRange::from(self);
sr.contains_range(range)
}
}
impl From<&Parameter> for SourceRange {

View File

@ -0,0 +1,406 @@
use serde::Serialize;
use super::{BodyItem, Expr, MemberObject, Node, Program};
use crate::SourceRange;
/// A traversal path through the AST to a node.
///
/// Similar to the idea of a `NodeId`, a `NodePath` uniquely identifies a node,
/// assuming you know the root node.
///
/// The implementation doesn't cover all parts of the tree. It currently only
/// works on parts of the tree that the frontend uses.
#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq, Hash, ts_rs::TS)]
#[ts(export_to = "NodePath.ts")]
pub struct NodePath {
pub steps: Vec<Step>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash, ts_rs::TS)]
#[ts(export_to = "NodePath.ts")]
#[serde(tag = "type")]
pub enum Step {
ProgramBodyItem { index: usize },
CallCallee,
CallArg { index: usize },
CallKwCallee,
CallKwUnlabeledArg,
CallKwArg { index: usize },
BinaryLeft,
BinaryRight,
UnaryArg,
PipeBodyItem { index: usize },
ArrayElement { index: usize },
ArrayRangeStart,
ArrayRangeEnd,
ObjectProperty { index: usize },
ObjectPropertyKey,
ObjectPropertyValue,
ExpressionStatementExpr,
VariableDeclarationDeclaration,
VariableDeclarationInit,
FunctionExpressionParam { index: usize },
FunctionExpressionBody,
FunctionExpressionBodyItem { index: usize },
ReturnStatementArg,
MemberExpressionObject,
MemberExpressionProperty,
IfExpressionCondition,
IfExpressionThen,
IfExpressionElseIf { index: usize },
IfExpressionElseIfCond,
IfExpressionElseIfBody,
IfExpressionElse,
ImportStatementItem { index: usize },
ImportStatementItemName,
ImportStatementItemAlias,
LabeledExpressionExpr,
LabeledExpressionLabel,
AscribedExpressionExpr,
}
impl NodePath {
/// Given a program and a [`SourceRange`], return the path to the node that
/// contains the range.
pub(crate) fn from_range(program: &Node<Program>, range: SourceRange) -> Option<Self> {
Self::from_body(&program.body, range, NodePath::default())
}
fn from_body(body: &[BodyItem], range: SourceRange, mut path: NodePath) -> Option<NodePath> {
for (i, item) in body.iter().enumerate() {
if item.contains_range(&range) {
path.push(Step::ProgramBodyItem { index: i });
return Self::from_body_item(item, range, path);
}
}
None
}
fn from_body_item(body_item: &BodyItem, range: SourceRange, mut path: NodePath) -> Option<NodePath> {
match body_item {
BodyItem::ImportStatement(node) => match &node.selector {
super::ImportSelector::List { items } => {
for (i, item) in items.iter().enumerate() {
if item.contains_range(&range) {
path.push(Step::ImportStatementItem { index: i });
if item.name.contains_range(&range) {
path.push(Step::ImportStatementItemName);
return Some(path);
}
if let Some(alias) = &item.alias {
if alias.contains_range(&range) {
path.push(Step::ImportStatementItemAlias);
return Some(path);
}
}
return Some(path);
}
}
}
super::ImportSelector::Glob(_) => {
// TODO: Handle glob imports.
}
super::ImportSelector::None { .. } => {
// TODO: Handle whole-module imports.
}
},
BodyItem::ExpressionStatement(node) => {
path.push(Step::ExpressionStatementExpr);
return Self::from_expr(&node.expression, range, path);
}
BodyItem::VariableDeclaration(node) => {
if node.declaration.contains_range(&range) {
path.push(Step::VariableDeclarationDeclaration);
if node.declaration.init.contains_range(&range) {
path.push(Step::VariableDeclarationInit);
return Self::from_expr(&node.declaration.init, range, path);
}
}
}
BodyItem::TypeDeclaration(_) => {}
BodyItem::ReturnStatement(node) => {
if node.argument.contains_range(&range) {
path.push(Step::ReturnStatementArg);
return Self::from_expr(&node.argument, range, path);
}
}
}
Some(path)
}
fn from_expr(expr: &Expr, range: SourceRange, mut path: NodePath) -> Option<NodePath> {
match expr {
Expr::Literal(node) => {
if node.contains_range(&range) {
return Some(path);
}
}
Expr::Name(node) => {
if node.contains_range(&range) {
return Some(path);
}
}
Expr::TagDeclarator(node) => {
if node.contains_range(&range) {
return Some(path);
}
}
Expr::BinaryExpression(node) => {
let left = Expr::from(&node.left);
if left.contains_range(&range) {
path.push(Step::BinaryLeft);
return Self::from_expr(&left, range, path);
}
let right = Expr::from(&node.right);
if right.contains_range(&range) {
path.push(Step::BinaryRight);
return Self::from_expr(&right, range, path);
}
}
Expr::FunctionExpression(node) => {
for (i, param) in node.params.iter().enumerate() {
// TODO: Check the type annotation and default value.
if param.contains_range(&range) {
path.push(Step::FunctionExpressionParam { index: i });
return Some(path);
}
}
if node.body.contains_range(&range) {
path.push(Step::FunctionExpressionBody);
for (i, item) in node.body.body.iter().enumerate() {
if item.contains_range(&range) {
path.push(Step::FunctionExpressionBodyItem { index: i });
return Self::from_body_item(item, range, path);
}
}
}
}
Expr::CallExpression(node) => {
if node.callee.contains_range(&range) {
path.push(Step::CallCallee);
return Some(path);
}
for (i, arg) in node.arguments.iter().enumerate() {
if arg.contains_range(&range) {
path.push(Step::CallArg { index: i });
return Self::from_expr(arg, range, path);
}
}
}
Expr::CallExpressionKw(node) => {
if node.callee.contains_range(&range) {
path.push(Step::CallKwCallee);
return Some(path);
}
if let Some(unlabeled) = &node.unlabeled {
if unlabeled.contains_range(&range) {
path.push(Step::CallKwUnlabeledArg);
return Self::from_expr(unlabeled, range, path);
}
}
for (i, arg) in node.arguments.iter().enumerate() {
if arg.arg.contains_range(&range) {
path.push(Step::CallKwArg { index: i });
return Self::from_expr(&arg.arg, range, path);
}
}
}
Expr::PipeExpression(node) => {
for (i, expr) in node.body.iter().enumerate() {
if expr.contains_range(&range) {
path.push(Step::PipeBodyItem { index: i });
return Self::from_expr(expr, range, path);
}
}
}
Expr::PipeSubstitution(_) => {}
Expr::ArrayExpression(node) => {
for (i, element) in node.elements.iter().enumerate() {
if element.contains_range(&range) {
path.push(Step::ArrayElement { index: i });
return Self::from_expr(element, range, path);
}
}
}
Expr::ArrayRangeExpression(node) => {
if node.start_element.contains_range(&range) {
path.push(Step::ArrayRangeStart);
return Self::from_expr(&node.start_element, range, path);
}
if node.end_element.contains_range(&range) {
path.push(Step::ArrayRangeEnd);
return Self::from_expr(&node.end_element, range, path);
}
}
Expr::ObjectExpression(node) => {
for (i, property) in node.properties.iter().enumerate() {
if property.contains_range(&range) {
path.push(Step::ObjectProperty { index: i });
if property.key.contains_range(&range) {
path.push(Step::ObjectPropertyKey);
return Some(path);
}
if property.value.contains_range(&range) {
path.push(Step::ObjectPropertyValue);
return Self::from_expr(&property.value, range, path);
}
return Some(path);
}
}
}
Expr::MemberExpression(node) => {
if node.object.contains_range(&range) {
path.push(Step::MemberExpressionObject);
return Self::from_member_expr_object(&node.object, range, path);
}
if node.property.contains_range(&range) {
path.push(Step::MemberExpressionProperty);
return Some(path);
}
}
Expr::UnaryExpression(node) => {
let arg = Expr::from(&node.argument);
if arg.contains_range(&range) {
path.push(Step::UnaryArg);
return Self::from_expr(&arg, range, path);
}
}
Expr::IfExpression(node) => {
if node.cond.contains_range(&range) {
path.push(Step::IfExpressionCondition);
return Self::from_expr(&node.cond, range, path);
}
if node.then_val.contains_range(&range) {
path.push(Step::IfExpressionThen);
return Self::from_body(&node.then_val.body, range, path);
}
for else_if in &node.else_ifs {
if else_if.contains_range(&range) {
path.push(Step::IfExpressionElseIf { index: 0 });
if else_if.cond.contains_range(&range) {
path.push(Step::IfExpressionElseIfCond);
return Self::from_expr(&else_if.cond, range, path);
}
if else_if.then_val.contains_range(&range) {
path.push(Step::IfExpressionElseIfBody);
return Self::from_body(&else_if.then_val.body, range, path);
}
return Some(path);
}
}
if node.final_else.contains_range(&range) {
path.push(Step::IfExpressionElse);
return Self::from_body(&node.final_else.body, range, path);
}
}
Expr::LabelledExpression(node) => {
if node.expr.contains_range(&range) {
path.push(Step::LabeledExpressionExpr);
return Self::from_expr(&node.expr, range, path);
}
if node.label.contains_range(&range) {
path.push(Step::LabeledExpressionLabel);
return Some(path);
}
}
Expr::AscribedExpression(node) => {
if node.expr.contains_range(&range) {
path.push(Step::AscribedExpressionExpr);
return Self::from_expr(&node.expr, range, path);
}
// TODO: Check the type annotation.
}
Expr::None(_) => {}
}
Some(path)
}
fn from_member_expr_object(mut expr: &MemberObject, range: SourceRange, mut path: NodePath) -> Option<NodePath> {
while let MemberObject::MemberExpression(node) = expr {
if !node.object.contains_range(&range) {
break;
}
path.push(Step::MemberExpressionObject);
expr = &node.object;
}
Some(path)
}
fn push(&mut self, step: Step) {
self.steps.push(step);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ModuleId;
fn range(start: usize, end: usize) -> SourceRange {
SourceRange::new(start, end, ModuleId::default())
}
#[test]
fn test_node_path_from_range() {
// Read the contents of the file.
let contents = std::fs::read_to_string("tests/misc/cube.kcl").unwrap();
let program = crate::Program::parse_no_errs(&contents).unwrap();
// fn cube(sideLength, center) {
// ^^^^
assert_eq!(
NodePath::from_range(&program.ast, range(38, 42)).unwrap(),
NodePath {
steps: vec![Step::ProgramBodyItem { index: 0 }, Step::VariableDeclarationDeclaration],
}
);
// fn cube(sideLength, center) {
// ^^^^^^
assert_eq!(
NodePath::from_range(&program.ast, range(55, 61)).unwrap(),
NodePath {
steps: vec![
Step::ProgramBodyItem { index: 0 },
Step::VariableDeclarationDeclaration,
Step::VariableDeclarationInit,
Step::FunctionExpressionParam { index: 1 }
],
}
);
// |> line(endAbsolute = p1)
// ^^
assert_eq!(
NodePath::from_range(&program.ast, range(293, 295)).unwrap(),
NodePath {
steps: vec![
Step::ProgramBodyItem { index: 0 },
Step::VariableDeclarationDeclaration,
Step::VariableDeclarationInit,
Step::FunctionExpressionBody,
Step::FunctionExpressionBodyItem { index: 7 },
Step::ReturnStatementArg,
Step::PipeBodyItem { index: 2 },
Step::CallKwArg { index: 0 },
],
}
);
// myCube = cube(sideLength = 40, center = [0, 0])
// ^
assert_eq!(
NodePath::from_range(&program.ast, range(485, 486)).unwrap(),
NodePath {
steps: vec![
Step::ProgramBodyItem { index: 1 },
Step::VariableDeclarationDeclaration,
Step::VariableDeclarationInit,
Step::CallKwArg { index: 1 },
Step::ArrayElement { index: 1 }
],
}
);
}
}