Skip to content

Commit

Permalink
revset: add a function for escaping a symbol
Browse files Browse the repository at this point in the history
When we have e.g. a bookmark name or a remote name and we want to
produce a revset from it, we may need to quote and escape it. This
patch implements a function for that.

Perhaps we'll want to more generally be able to format a whole
`RevsetExpression` later.
  • Loading branch information
martinvonz committed Jan 30, 2025
1 parent a6d0786 commit 2854131
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 0 deletions.
18 changes: 18 additions & 0 deletions lib/src/dsl_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,24 @@ impl<R: RuleType> StringLiteralParser<R> {
}
}

/// Escape special characters in the input
pub fn escape_string(unescaped: &str) -> String {
let mut escaped = String::with_capacity(unescaped.len());
for c in unescaped.chars() {
match c {
'"' => escaped.push_str(r#"\""#),
'\\' => escaped.push_str(r#"\\"#),
'\t' => escaped.push_str(r#"\t"#),
'\r' => escaped.push_str(r#"\r"#),
'\n' => escaped.push_str(r#"\n"#),
'\0' => escaped.push_str(r#"\0"#),
'\x1b' => escaped.push_str(r#"\e"#),
c => escaped.push(c),
}
}
escaped
}

/// Helper to parse function call.
#[derive(Debug)]
pub struct FunctionCallParser<R> {
Expand Down
34 changes: 34 additions & 0 deletions lib/src/revset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2686,6 +2686,15 @@ pub struct RevsetWorkspaceContext<'a> {
pub workspace_id: &'a WorkspaceId,
}

/// Formats a string as symbol by quoting and escaping it if necessary
pub fn format_symbol(literal: &str) -> String {
if revset_parser::is_identifier(literal) {
literal.to_string()
} else {
format!(r#""{}""#, dsl_util::escape_string(literal))
}
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;
Expand Down Expand Up @@ -4327,4 +4336,29 @@ mod tests {
}
"###);
}

#[test]
fn test_escape_string_literal() {
// Valid identifiers don't need quoting
assert_eq!(format_symbol("foo"), "foo");
assert_eq!(format_symbol("foo.bar"), "foo.bar");

// Invalid identifiers need quoting
assert_eq!(format_symbol("foo@bar"), r#""foo@bar""#);
assert_eq!(format_symbol("foo bar"), r#""foo bar""#);
assert_eq!(format_symbol("(foo)"), r#""(foo)""#);
assert_eq!(format_symbol("all:foo"), r#""all:foo""#);

// Some characters also need escaping
assert_eq!(format_symbol(r#"foo"bar"#), r#""foo\"bar""#);
assert_eq!(format_symbol(r#"foo\bar"#), r#""foo\\bar""#);
assert_eq!(format_symbol(r#"foo\"bar"#), r#""foo\\\"bar""#);
assert_eq!(format_symbol("foo\nbar"), r#""foo\nbar""#);

// Some characters don't technically need escaping, but we escape them for
// clarity
assert_eq!(format_symbol(r#"foo"bar"#), r#""foo\"bar""#);
assert_eq!(format_symbol(r#"foo\bar"#), r#""foo\\bar""#);
assert_eq!(format_symbol(r#"foo\"bar"#), r#""foo\\\"bar""#);
}
}
8 changes: 8 additions & 0 deletions lib/src/revset_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,14 @@ fn parse_as_string_literal(pair: Pair<Rule>) -> String {
}
}

/// Checks if the text is a valid identifier
pub fn is_identifier(text: &str) -> bool {
match RevsetParser::parse(Rule::identifier, text) {
Ok(mut pairs) => pairs.next().unwrap().as_span().end() == text.len(),
Err(_) => false,
}
}

pub type RevsetAliasesMap = AliasesMap<RevsetAliasParser, String>;

#[derive(Clone, Debug, Default)]
Expand Down

0 comments on commit 2854131

Please sign in to comment.