Skip to content

Distinguish different types of Nix paths at the type level #172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ mod interpol;
mod nodes;
mod operators;
mod path_util;

pub use path_util::Path;
mod str_util;
mod tokens;

Expand Down
20 changes: 18 additions & 2 deletions src/ast/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,10 @@ node! {
IfElse,
Select,
Str,
Path,
PathAbs,
PathRel,
PathHome,
PathSearch,
Literal,
Lambda,
LegacyLet,
Expand Down Expand Up @@ -279,7 +282,20 @@ impl InheritFrom {
tg! { r_paren_token, ')' }
}

node! { #[from(NODE_PATH)] struct Path; }
node! { #[from(NODE_PATH_ABS)] struct PathAbs; }
node! { #[from(NODE_PATH_REL)] struct PathRel; }
node! { #[from(NODE_PATH_HOME)] struct PathHome; }
node! { #[from(NODE_PATH_SEARCH)] struct PathSearch; }

node! {
#[from(
PathAbs,
PathRel,
PathHome,
PathSearch,
)]
enum Path;
}

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

Expand Down
201 changes: 170 additions & 31 deletions src/ast/path_util.rs
Original file line number Diff line number Diff line change
@@ -1,65 +1,204 @@
use crate::{ast::AstToken, kinds::SyntaxKind::*};
use crate::kinds::SyntaxKind::{TOKEN_PATH_ABS, TOKEN_PATH_HOME, TOKEN_PATH_REL};

use rowan::{ast::AstNode as OtherAstNode, NodeOrToken};

pub use super::nodes::Path;
use super::{
nodes::{PathAbs, PathHome, PathRel, PathSearch},
AstToken, InterpolPart, PathContent,
};
use crate::ast;

use super::{InterpolPart, PathContent};

impl ast::nodes::Path {
pub fn parts(&self) -> impl Iterator<Item = InterpolPart<PathContent>> {
self.syntax().children_with_tokens().map(|child| match child {
fn extract_path_parts<T: ast::AstNode>(node: &T) -> Vec<InterpolPart<PathContent>> {
node.syntax()
.children_with_tokens()
.map(|child| match child {
NodeOrToken::Token(token) => {
assert_eq!(token.kind(), TOKEN_PATH);
debug_assert!(matches!(
token.kind(),
TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME
));
InterpolPart::Literal(PathContent::cast(token).unwrap())
}
NodeOrToken::Node(node) => {
InterpolPart::Interpolation(ast::Interpol::cast(node.clone()).unwrap())
}
})
.collect()
}

// Direct methods for interpolatable path types
impl PathAbs {
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
extract_path_parts(self)
}
}

impl PathRel {
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
extract_path_parts(self)
}
}

impl PathHome {
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
extract_path_parts(self)
}
}

// Direct methods for search path
impl PathSearch {
/// Get the content of a search path
pub fn content(&self) -> Option<PathContent> {
self.syntax()
.children_with_tokens()
.filter_map(|child| child.into_token().and_then(PathContent::cast))
.next()
}
}

/// Extension methods for the Path enum
impl Path {
/// Get parts from any path type in a unified way
pub fn parts(&self) -> Vec<InterpolPart<PathContent>> {
match self {
// For interpolatable paths, get their parts
Path::PathAbs(p) => p.parts(),
Path::PathRel(p) => p.parts(),
Path::PathHome(p) => p.parts(),
// For search paths, return a single literal component if content exists
Path::PathSearch(p) => {
if let Some(content) = p.content() {
vec![InterpolPart::Literal(content)]
} else {
vec![]
}
}
}
}

pub fn is_search(&self) -> bool {
matches!(self, Path::PathSearch(_))
}

pub fn is_interpolatable(&self) -> bool {
!self.is_search()
}
}

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

use super::InterpolPart;
use crate::{
ast::{self, AstToken, InterpolPart, PathContent},
ast::{self, Path},
Root,
};

#[test]
fn parts() {
fn assert_eq_ast_ctn(it: &mut dyn Iterator<Item = InterpolPart<PathContent>>, x: &str) {
let tmp = it.next().expect("unexpected EOF");
if let InterpolPart::Interpolation(astn) = tmp {
assert_eq!(astn.expr().unwrap().syntax().to_string(), x);
} else {
unreachable!("unexpected literal {:?}", tmp);
fn test_path_types() {
// Absolute path
let inp = "/foo/bar";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathAbs(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();
assert!(path.is_interpolatable());
assert!(!path.is_search());
}

// Search path
let inp = "<nixpkgs>";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathSearch(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();
assert!(!path.is_interpolatable());
assert!(path.is_search());
}
}

#[test]
fn test_parts() {
// Test parts with absolute path
let inp = "/foo/bar";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathAbs(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();

let parts = path.parts();
assert_eq!(parts.len(), 1);

match &parts[0] {
InterpolPart::Literal(content) => {
assert_eq!(content.text(), "/foo/bar");
}
_ => panic!("Expected literal part"),
}
}

fn assert_eq_lit(it: &mut dyn Iterator<Item = InterpolPart<PathContent>>, x: &str) {
let tmp = it.next().expect("unexpected EOF");
if let InterpolPart::Literal(astn) = tmp {
assert_eq!(astn.syntax().text(), x);
} else {
unreachable!("unexpected interpol {:?}", tmp);
// Test parts with interpolated path
let inp = r#"./a/${"hello"}"#;
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathRel(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();

let parts = path.parts();
assert_eq!(parts.len(), 2);

match &parts[0] {
InterpolPart::Literal(content) => {
assert_eq!(content.text(), "./a/");
}
_ => panic!("Expected literal part"),
}

match &parts[1] {
InterpolPart::Interpolation(_) => {} // Success
_ => panic!("Expected interpolation part"),
}
}

let inp = r#"./a/b/${"c"}/${d}/e/f"#;
// Test parts with search path
let inp = "<nixpkgs>";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
match expr {
ast::Expr::Path(p) => {
let mut it = p.parts();
assert_eq_lit(&mut it, "./a/b/");
assert_eq_ast_ctn(&mut it, "\"c\"");
assert_eq_lit(&mut it, "/");
assert_eq_ast_ctn(&mut it, "d");
assert_eq_lit(&mut it, "/e/f");
if let ast::Expr::PathSearch(p) = expr {
let path = Path::cast(p.syntax().clone()).unwrap();

let parts = path.parts();
assert_eq!(parts.len(), 1);

match &parts[0] {
InterpolPart::Literal(content) => {
assert_eq!(content.text(), "<nixpkgs>");
}
_ => panic!("Expected literal part"),
}
}
}

#[test]
fn direct_method_usage() {
// Test direct parts() method on PathAbs
let inp = "/foo/bar";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathAbs(p) = expr {
let parts = p.parts();
assert_eq!(parts.len(), 1);

match &parts[0] {
InterpolPart::Literal(content) => {
assert_eq!(content.text(), "/foo/bar");
}
_ => panic!("Expected literal part"),
}
_ => unreachable!(),
}

// Test direct content() method on PathSearch
let inp = "<nixpkgs>";
let expr = Root::parse(inp).ok().unwrap().expr().unwrap();
if let ast::Expr::PathSearch(p) = expr {
let content = p.content().expect("Expected content");
assert_eq!(content.text(), "<nixpkgs>");
}
}
}
55 changes: 54 additions & 1 deletion src/ast/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,60 @@ impl Integer {
}
}

token! { #[from(TOKEN_PATH)] struct PathContent; }
// A literal part of a path (absolute / relative / home / search) without
// distinguishing which concrete flavour it was taken from.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PathContent(pub(super) SyntaxToken);

impl PathContent {
/// Returns the path as a string
pub fn text(&self) -> &str {
self.syntax().text()
}

/// Returns the kind of path token
pub fn path_kind(&self) -> SyntaxKind {
self.syntax().kind()
}

/// Returns true if this is an absolute path
pub fn is_absolute(&self) -> bool {
self.path_kind() == TOKEN_PATH_ABS
}

/// Returns true if this is a relative path
pub fn is_relative(&self) -> bool {
self.path_kind() == TOKEN_PATH_REL
}

/// Returns true if this is a home-relative path
pub fn is_home(&self) -> bool {
self.path_kind() == TOKEN_PATH_HOME
}

/// Returns true if this is a search path
pub fn is_search(&self) -> bool {
self.path_kind() == TOKEN_PATH_SEARCH
}
}

impl AstToken for PathContent {
fn can_cast(kind: SyntaxKind) -> bool {
matches!(kind, TOKEN_PATH_ABS | TOKEN_PATH_REL | TOKEN_PATH_HOME | TOKEN_PATH_SEARCH)
}

fn cast(from: SyntaxToken) -> Option<Self> {
if Self::can_cast(from.kind()) {
Some(Self(from))
} else {
None
}
}

fn syntax(&self) -> &SyntaxToken {
&self.0
}
}

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

Expand Down
Loading
Loading