Skip to content

Commit f5e0873

Browse files
committed
Distinguish different types of Nix paths at the type level
- Added distinct token and node types for different path varieties: - PathAbs: Absolute paths starting with `/` (e.g., `/nix/store/...`) - PathRel: Relative paths (e.g., `./foo`, `foo/bar`) - PathHome: Home-relative paths starting with `~/` (e.g., `~/foo/bar`) - PathSearch: NIX_PATH expressions (e.g., `<nixpkgs>`) - Created `InterpolatablePath` trait for paths that support interpolation (all except search paths) - Added helper methods to path content for easier path type checking and access through `match_path`, `is_search`, `is_interpolatable`, etc. This allows code to determine what kind of path it's dealing with without having to re-parse the path string, improving type safety, correctness, and making path-specific operations more explicit.
1 parent e049bd2 commit f5e0873

31 files changed

+460
-127
lines changed

flake.lock

Lines changed: 24 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ast.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ mod interpol;
55
mod nodes;
66
mod operators;
77
mod path_util;
8+
9+
pub use path_util::{Path, PathNode};
810
mod str_util;
911
mod tokens;
1012

src/ast/nodes.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,10 @@ node! {
176176
IfElse,
177177
Select,
178178
Str,
179-
Path,
179+
PathAbs,
180+
PathRel,
181+
PathHome,
182+
PathSearch,
180183
Literal,
181184
Lambda,
182185
LegacyLet,
@@ -279,7 +282,20 @@ impl InheritFrom {
279282
tg! { r_paren_token, ')' }
280283
}
281284

282-
node! { #[from(NODE_PATH)] struct Path; }
285+
node! { #[from(NODE_PATH_ABS)] struct PathAbs; }
286+
node! { #[from(NODE_PATH_REL)] struct PathRel; }
287+
node! { #[from(NODE_PATH_HOME)] struct PathHome; }
288+
node! { #[from(NODE_PATH_SEARCH)] struct PathSearch; }
289+
290+
node! {
291+
#[from(
292+
PathAbs,
293+
PathRel,
294+
PathHome,
295+
PathSearch,
296+
)]
297+
enum Path;
298+
}
283299

284300
node! { #[from(NODE_STRING)] struct Str; }
285301

src/ast/path_util.rs

Lines changed: 180 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,214 @@
1-
use crate::{ast::AstToken, kinds::SyntaxKind::*};
1+
use crate::kinds::SyntaxKind::{TOKEN_PATH_ABS, TOKEN_PATH_HOME, TOKEN_PATH_REL};
2+
23
use rowan::{ast::AstNode as OtherAstNode, NodeOrToken};
34

5+
pub use super::nodes::Path;
6+
use super::{
7+
nodes::{PathAbs, PathHome, PathRel, PathSearch},
8+
AstToken, InterpolPart, PathContent,
9+
};
410
use crate::ast;
511

6-
use super::{InterpolPart, PathContent};
12+
/// Base trait for all path node types
13+
pub trait PathNode: ast::AstNode {}
14+
15+
impl PathNode for PathAbs {}
16+
impl PathNode for PathRel {}
17+
impl PathNode for PathHome {}
18+
impl PathNode for PathSearch {}
19+
impl PathNode for Path {}
720

8-
impl ast::nodes::Path {
9-
pub fn parts(&self) -> impl Iterator<Item = InterpolPart<PathContent>> {
10-
self.syntax().children_with_tokens().map(|child| match child {
21+
22+
fn extract_path_parts<T: ast::AstNode>(node: &T) -> Vec<InterpolPart<PathContent>> {
23+
node.syntax()
24+
.children_with_tokens()
25+
.map(|child| match child {
1126
NodeOrToken::Token(token) => {
12-
assert_eq!(token.kind(), TOKEN_PATH);
27+
debug_assert!(matches!(
28+
token.kind(),
29+
TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME
30+
));
1331
InterpolPart::Literal(PathContent::cast(token).unwrap())
1432
}
1533
NodeOrToken::Node(node) => {
1634
InterpolPart::Interpolation(ast::Interpol::cast(node.clone()).unwrap())
1735
}
1836
})
37+
.collect()
38+
}
39+
40+
// Direct methods for interpolatable path types
41+
impl PathAbs {
42+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
43+
extract_path_parts(self)
44+
}
45+
}
46+
47+
impl PathRel {
48+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
49+
extract_path_parts(self)
50+
}
51+
}
52+
53+
impl PathHome {
54+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
55+
extract_path_parts(self)
56+
}
57+
}
58+
59+
// Direct methods for search path
60+
impl PathSearch {
61+
/// Get the content of a search path
62+
pub fn content(&self) -> Option<PathContent> {
63+
self.syntax()
64+
.children_with_tokens()
65+
.filter_map(|child| child.into_token().and_then(PathContent::cast))
66+
.next()
67+
}
68+
}
69+
70+
/// Extension methods for the Path enum
71+
impl Path {
72+
/// Get parts from any path type in a unified way
73+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
74+
match self {
75+
// For interpolatable paths, get their parts
76+
Path::PathAbs(p) => p.parts(),
77+
Path::PathRel(p) => p.parts(),
78+
Path::PathHome(p) => p.parts(),
79+
// For search paths, return a single literal component if content exists
80+
Path::PathSearch(p) => {
81+
if let Some(content) = p.content() {
82+
vec![InterpolPart::Literal(content)]
83+
} else {
84+
vec![]
85+
}
86+
}
87+
}
88+
}
89+
90+
pub fn is_search(&self) -> bool {
91+
matches!(self, Path::PathSearch(_))
92+
}
93+
94+
pub fn is_interpolatable(&self) -> bool {
95+
!self.is_search()
1996
}
2097
}
2198

2299
#[cfg(test)]
23100
mod tests {
24101
use rowan::ast::AstNode;
25102

103+
use super::InterpolPart;
26104
use crate::{
27-
ast::{self, AstToken, InterpolPart, PathContent},
105+
ast::{self, Path},
28106
Root,
29107
};
30108

31109
#[test]
32-
fn parts() {
33-
fn assert_eq_ast_ctn(it: &mut dyn Iterator<Item = InterpolPart<PathContent>>, x: &str) {
34-
let tmp = it.next().expect("unexpected EOF");
35-
if let InterpolPart::Interpolation(astn) = tmp {
36-
assert_eq!(astn.expr().unwrap().syntax().to_string(), x);
37-
} else {
38-
unreachable!("unexpected literal {:?}", tmp);
110+
fn test_path_types() {
111+
// Absolute path
112+
let inp = "/foo/bar";
113+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
114+
if let ast::Expr::PathAbs(p) = expr {
115+
let path = Path::cast(p.syntax().clone()).unwrap();
116+
assert!(path.is_interpolatable());
117+
assert!(!path.is_search());
118+
}
119+
120+
// Search path
121+
let inp = "<nixpkgs>";
122+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
123+
if let ast::Expr::PathSearch(p) = expr {
124+
let path = Path::cast(p.syntax().clone()).unwrap();
125+
assert!(!path.is_interpolatable());
126+
assert!(path.is_search());
127+
}
128+
}
129+
130+
#[test]
131+
fn test_parts() {
132+
// Test parts with absolute path
133+
let inp = "/foo/bar";
134+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
135+
if let ast::Expr::PathAbs(p) = expr {
136+
let path = Path::cast(p.syntax().clone()).unwrap();
137+
138+
let parts = path.parts();
139+
assert_eq!(parts.len(), 1);
140+
141+
match &parts[0] {
142+
InterpolPart::Literal(content) => {
143+
assert_eq!(content.text(), "/foo/bar");
144+
}
145+
_ => panic!("Expected literal part"),
146+
}
147+
}
148+
149+
// Test parts with interpolated path
150+
let inp = r#"./a/${"hello"}"#;
151+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
152+
if let ast::Expr::PathRel(p) = expr {
153+
let path = Path::cast(p.syntax().clone()).unwrap();
154+
155+
let parts = path.parts();
156+
assert_eq!(parts.len(), 2);
157+
158+
match &parts[0] {
159+
InterpolPart::Literal(content) => {
160+
assert_eq!(content.text(), "./a/");
161+
}
162+
_ => panic!("Expected literal part"),
163+
}
164+
165+
match &parts[1] {
166+
InterpolPart::Interpolation(_) => {} // Success
167+
_ => panic!("Expected interpolation part"),
39168
}
40169
}
41170

42-
fn assert_eq_lit(it: &mut dyn Iterator<Item = InterpolPart<PathContent>>, x: &str) {
43-
let tmp = it.next().expect("unexpected EOF");
44-
if let InterpolPart::Literal(astn) = tmp {
45-
assert_eq!(astn.syntax().text(), x);
46-
} else {
47-
unreachable!("unexpected interpol {:?}", tmp);
171+
// Test parts with search path
172+
let inp = "<nixpkgs>";
173+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
174+
if let ast::Expr::PathSearch(p) = expr {
175+
let path = Path::cast(p.syntax().clone()).unwrap();
176+
177+
let parts = path.parts();
178+
assert_eq!(parts.len(), 1);
179+
180+
match &parts[0] {
181+
InterpolPart::Literal(content) => {
182+
assert_eq!(content.text(), "<nixpkgs>");
183+
}
184+
_ => panic!("Expected literal part"),
48185
}
49186
}
187+
}
50188

51-
let inp = r#"./a/b/${"c"}/${d}/e/f"#;
189+
#[test]
190+
fn direct_method_usage() {
191+
// Test direct parts() method on PathAbs
192+
let inp = "/foo/bar";
52193
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
53-
match expr {
54-
ast::Expr::Path(p) => {
55-
let mut it = p.parts();
56-
assert_eq_lit(&mut it, "./a/b/");
57-
assert_eq_ast_ctn(&mut it, "\"c\"");
58-
assert_eq_lit(&mut it, "/");
59-
assert_eq_ast_ctn(&mut it, "d");
60-
assert_eq_lit(&mut it, "/e/f");
194+
if let ast::Expr::PathAbs(p) = expr {
195+
let parts = p.parts();
196+
assert_eq!(parts.len(), 1);
197+
198+
match &parts[0] {
199+
InterpolPart::Literal(content) => {
200+
assert_eq!(content.text(), "/foo/bar");
201+
}
202+
_ => panic!("Expected literal part"),
61203
}
62-
_ => unreachable!(),
204+
}
205+
206+
// Test direct content() method on PathSearch
207+
let inp = "<nixpkgs>";
208+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
209+
if let ast::Expr::PathSearch(p) = expr {
210+
let content = p.content().expect("Expected content");
211+
assert_eq!(content.text(), "<nixpkgs>");
63212
}
64213
}
65-
}
214+
}

0 commit comments

Comments
 (0)