Skip to content

Commit 588d0b5

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 588d0b5

31 files changed

+450
-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;
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: 170 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,204 @@
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};
7-
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 {
12+
fn extract_path_parts<T: ast::AstNode>(node: &T) -> Vec<InterpolPart<PathContent>> {
13+
node.syntax()
14+
.children_with_tokens()
15+
.map(|child| match child {
1116
NodeOrToken::Token(token) => {
12-
assert_eq!(token.kind(), TOKEN_PATH);
17+
debug_assert!(matches!(
18+
token.kind(),
19+
TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME
20+
));
1321
InterpolPart::Literal(PathContent::cast(token).unwrap())
1422
}
1523
NodeOrToken::Node(node) => {
1624
InterpolPart::Interpolation(ast::Interpol::cast(node.clone()).unwrap())
1725
}
1826
})
27+
.collect()
28+
}
29+
30+
// Direct methods for interpolatable path types
31+
impl PathAbs {
32+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
33+
extract_path_parts(self)
34+
}
35+
}
36+
37+
impl PathRel {
38+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
39+
extract_path_parts(self)
40+
}
41+
}
42+
43+
impl PathHome {
44+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
45+
extract_path_parts(self)
46+
}
47+
}
48+
49+
// Direct methods for search path
50+
impl PathSearch {
51+
/// Get the content of a search path
52+
pub fn content(&self) -> Option<PathContent> {
53+
self.syntax()
54+
.children_with_tokens()
55+
.filter_map(|child| child.into_token().and_then(PathContent::cast))
56+
.next()
57+
}
58+
}
59+
60+
/// Extension methods for the Path enum
61+
impl Path {
62+
/// Get parts from any path type in a unified way
63+
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
64+
match self {
65+
// For interpolatable paths, get their parts
66+
Path::PathAbs(p) => p.parts(),
67+
Path::PathRel(p) => p.parts(),
68+
Path::PathHome(p) => p.parts(),
69+
// For search paths, return a single literal component if content exists
70+
Path::PathSearch(p) => {
71+
if let Some(content) = p.content() {
72+
vec![InterpolPart::Literal(content)]
73+
} else {
74+
vec![]
75+
}
76+
}
77+
}
78+
}
79+
80+
pub fn is_search(&self) -> bool {
81+
matches!(self, Path::PathSearch(_))
82+
}
83+
84+
pub fn is_interpolatable(&self) -> bool {
85+
!self.is_search()
1986
}
2087
}
2188

2289
#[cfg(test)]
2390
mod tests {
2491
use rowan::ast::AstNode;
2592

93+
use super::InterpolPart;
2694
use crate::{
27-
ast::{self, AstToken, InterpolPart, PathContent},
95+
ast::{self, Path},
2896
Root,
2997
};
3098

3199
#[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);
100+
fn test_path_types() {
101+
// Absolute path
102+
let inp = "/foo/bar";
103+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
104+
if let ast::Expr::PathAbs(p) = expr {
105+
let path = Path::cast(p.syntax().clone()).unwrap();
106+
assert!(path.is_interpolatable());
107+
assert!(!path.is_search());
108+
}
109+
110+
// Search path
111+
let inp = "<nixpkgs>";
112+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
113+
if let ast::Expr::PathSearch(p) = expr {
114+
let path = Path::cast(p.syntax().clone()).unwrap();
115+
assert!(!path.is_interpolatable());
116+
assert!(path.is_search());
117+
}
118+
}
119+
120+
#[test]
121+
fn test_parts() {
122+
// Test parts with absolute path
123+
let inp = "/foo/bar";
124+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
125+
if let ast::Expr::PathAbs(p) = expr {
126+
let path = Path::cast(p.syntax().clone()).unwrap();
127+
128+
let parts = path.parts();
129+
assert_eq!(parts.len(), 1);
130+
131+
match &parts[0] {
132+
InterpolPart::Literal(content) => {
133+
assert_eq!(content.text(), "/foo/bar");
134+
}
135+
_ => panic!("Expected literal part"),
39136
}
40137
}
41138

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);
139+
// Test parts with interpolated path
140+
let inp = r#"./a/${"hello"}"#;
141+
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
142+
if let ast::Expr::PathRel(p) = expr {
143+
let path = Path::cast(p.syntax().clone()).unwrap();
144+
145+
let parts = path.parts();
146+
assert_eq!(parts.len(), 2);
147+
148+
match &parts[0] {
149+
InterpolPart::Literal(content) => {
150+
assert_eq!(content.text(), "./a/");
151+
}
152+
_ => panic!("Expected literal part"),
153+
}
154+
155+
match &parts[1] {
156+
InterpolPart::Interpolation(_) => {} // Success
157+
_ => panic!("Expected interpolation part"),
48158
}
49159
}
50160

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

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)