Skip to content

Commit 45b4702

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 45b4702

31 files changed

+458
-126
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: 178 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,213 @@
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+
fn extract_path_parts<T: ast::AstNode>(node: &T) -> Vec<InterpolPart<PathContent>> {
22+
node.syntax()
23+
.children_with_tokens()
24+
.map(|child| match child {
1125
NodeOrToken::Token(token) => {
12-
assert_eq!(token.kind(), TOKEN_PATH);
26+
debug_assert!(matches!(
27+
token.kind(),
28+
TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME
29+
));
1330
InterpolPart::Literal(PathContent::cast(token).unwrap())
1431
}
1532
NodeOrToken::Node(node) => {
1633
InterpolPart::Interpolation(ast::Interpol::cast(node.clone()).unwrap())
1734
}
1835
})
36+
.collect()
37+
}
38+
39+
// Direct methods for interpolatable path types
40+
impl PathAbs {
41+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
42+
extract_path_parts(self)
43+
}
44+
}
45+
46+
impl PathRel {
47+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
48+
extract_path_parts(self)
49+
}
50+
}
51+
52+
impl PathHome {
53+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
54+
extract_path_parts(self)
55+
}
56+
}
57+
58+
// Direct methods for search path
59+
impl PathSearch {
60+
/// Get the content of a search path
61+
pub fn content(&self) -> Option<PathContent> {
62+
self.syntax()
63+
.children_with_tokens()
64+
.filter_map(|child| child.into_token().and_then(PathContent::cast))
65+
.next()
66+
}
67+
}
68+
69+
/// Extension methods for the Path enum
70+
impl Path {
71+
/// Get parts from any path type in a unified way
72+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
73+
match self {
74+
// For interpolatable paths, get their parts
75+
Path::PathAbs(p) => p.parts(),
76+
Path::PathRel(p) => p.parts(),
77+
Path::PathHome(p) => p.parts(),
78+
// For search paths, return a single literal component if content exists
79+
Path::PathSearch(p) => {
80+
if let Some(content) = p.content() {
81+
vec![InterpolPart::Literal(content)]
82+
} else {
83+
vec![]
84+
}
85+
}
86+
}
87+
}
88+
89+
pub fn is_search(&self) -> bool {
90+
matches!(self, Path::PathSearch(_))
91+
}
92+
93+
pub fn is_interpolatable(&self) -> bool {
94+
!self.is_search()
1995
}
2096
}
2197

2298
#[cfg(test)]
2399
mod tests {
24100
use rowan::ast::AstNode;
25101

102+
use super::InterpolPart;
26103
use crate::{
27-
ast::{self, AstToken, InterpolPart, PathContent},
104+
ast::{self, Path},
28105
Root,
29106
};
30107

31108
#[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);
109+
fn test_path_types() {
110+
// Absolute path
111+
let inp = "/foo/bar";
112+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
113+
if let ast::Expr::PathAbs(p) = expr {
114+
let path = Path::cast(p.syntax().clone()).unwrap();
115+
assert!(path.is_interpolatable());
116+
assert!(!path.is_search());
117+
}
118+
119+
// Search path
120+
let inp = "<nixpkgs>";
121+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
122+
if let ast::Expr::PathSearch(p) = expr {
123+
let path = Path::cast(p.syntax().clone()).unwrap();
124+
assert!(!path.is_interpolatable());
125+
assert!(path.is_search());
126+
}
127+
}
128+
129+
#[test]
130+
fn test_parts() {
131+
// Test parts with absolute path
132+
let inp = "/foo/bar";
133+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
134+
if let ast::Expr::PathAbs(p) = expr {
135+
let path = Path::cast(p.syntax().clone()).unwrap();
136+
137+
let parts = path.parts();
138+
assert_eq!(parts.len(), 1);
139+
140+
match &parts[0] {
141+
InterpolPart::Literal(content) => {
142+
assert_eq!(content.text(), "/foo/bar");
143+
}
144+
_ => panic!("Expected literal part"),
39145
}
40146
}
41147

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);
148+
// Test parts with interpolated path
149+
let inp = r#"./a/${"hello"}"#;
150+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
151+
if let ast::Expr::PathRel(p) = expr {
152+
let path = Path::cast(p.syntax().clone()).unwrap();
153+
154+
let parts = path.parts();
155+
assert_eq!(parts.len(), 2);
156+
157+
match &parts[0] {
158+
InterpolPart::Literal(content) => {
159+
assert_eq!(content.text(), "./a/");
160+
}
161+
_ => panic!("Expected literal part"),
162+
}
163+
164+
match &parts[1] {
165+
InterpolPart::Interpolation(_) => {} // Success
166+
_ => panic!("Expected interpolation part"),
48167
}
49168
}
50169

51-
let inp = r#"./a/b/${"c"}/${d}/e/f"#;
170+
// Test parts with search path
171+
let inp = "<nixpkgs>";
52172
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");
173+
if let ast::Expr::PathSearch(p) = expr {
174+
let path = Path::cast(p.syntax().clone()).unwrap();
175+
176+
let parts = path.parts();
177+
assert_eq!(parts.len(), 1);
178+
179+
match &parts[0] {
180+
InterpolPart::Literal(content) => {
181+
assert_eq!(content.text(), "<nixpkgs>");
182+
}
183+
_ => panic!("Expected literal part"),
184+
}
185+
}
186+
}
187+
188+
#[test]
189+
fn direct_method_usage() {
190+
// Test direct parts() method on PathAbs
191+
let inp = "/foo/bar";
192+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
193+
if let ast::Expr::PathAbs(p) = expr {
194+
let parts = p.parts();
195+
assert_eq!(parts.len(), 1);
196+
197+
match &parts[0] {
198+
InterpolPart::Literal(content) => {
199+
assert_eq!(content.text(), "/foo/bar");
200+
}
201+
_ => panic!("Expected literal part"),
61202
}
62-
_ => unreachable!(),
203+
}
204+
205+
// Test direct content() method on PathSearch
206+
let inp = "<nixpkgs>";
207+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
208+
if let ast::Expr::PathSearch(p) = expr {
209+
let content = p.content().expect("Expected content");
210+
assert_eq!(content.text(), "<nixpkgs>");
63211
}
64212
}
65213
}

src/ast/tokens.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,60 @@ impl Integer {
114114
}
115115
}
116116

117-
token! { #[from(TOKEN_PATH)] struct PathContent; }
117+
// A literal part of a path (absolute / relative / home / search) without
118+
// distinguishing which concrete flavour it was taken from.
119+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120+
pub struct PathContent(pub(super) SyntaxToken);
121+
122+
impl PathContent {
123+
/// Returns the path as a string
124+
pub fn text(&self) -> &str {
125+
self.syntax().text()
126+
}
127+
128+
/// Returns the kind of path token
129+
pub fn path_kind(&self) -> SyntaxKind {
130+
self.syntax().kind()
131+
}
132+
133+
/// Returns true if this is an absolute path
134+
pub fn is_absolute(&self) -> bool {
135+
self.path_kind() == TOKEN_PATH_ABS
136+
}
137+
138+
/// Returns true if this is a relative path
139+
pub fn is_relative(&self) -> bool {
140+
self.path_kind() == TOKEN_PATH_REL
141+
}
142+
143+
/// Returns true if this is a home-relative path
144+
pub fn is_home(&self) -> bool {
145+
self.path_kind() == TOKEN_PATH_HOME
146+
}
147+
148+
/// Returns true if this is a search path
149+
pub fn is_search(&self) -> bool {
150+
self.path_kind() == TOKEN_PATH_SEARCH
151+
}
152+
}
153+
154+
impl AstToken for PathContent {
155+
fn can_cast(kind: SyntaxKind) -> bool {
156+
matches!(kind, TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME | TOKEN_PATH_SEARCH)
157+
}
158+
159+
fn cast(from: SyntaxToken) -> Option<Self> {
160+
if Self::can_cast(from.kind()) {
161+
Some(Self(from))
162+
} else {
163+
None
164+
}
165+
}
166+
167+
fn syntax(&self) -> &SyntaxToken {
168+
&self.0
169+
}
170+
}
118171

119172
token! { #[from(TOKEN_STRING_CONTENT)] struct StrContent; }
120173

0 commit comments

Comments
 (0)