Skip to content

Commit 20f988b

Browse files
committed
fix(r-extractor): index-based quote strip preserves inner quote (#1102)
1 parent 58b85e7 commit 20f988b

1 file changed

Lines changed: 32 additions & 3 deletions

File tree

  • crates/codegraph-core/src/extractors

crates/codegraph-core/src/extractors/r_lang.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,22 @@ fn strip_string_quotes(node: &Node, source: &[u8]) -> String {
263263
if let Some(content) = find_child(node, "string_content") {
264264
return node_text(&content, source).to_string();
265265
}
266-
node_text(node, source)
267-
.trim_matches(|c| c == '\'' || c == '"')
268-
.to_string()
266+
// Fallback: strip exactly one matching quote from each end. We can't use
267+
// `trim_matches` because it strips *all* matching characters greedily —
268+
// e.g. for the literal `"'"` (a string containing a single quote) the
269+
// text is `"`, `'`, `"`, and `trim_matches` would consume all three,
270+
// returning an empty string. Index-based strip removes only the outer
271+
// pair, leaving the inner character intact.
272+
let text = node_text(node, source);
273+
let bytes = text.as_bytes();
274+
if bytes.len() >= 2 {
275+
let first = bytes[0];
276+
let last = bytes[bytes.len() - 1];
277+
if (first == b'\'' || first == b'"') && first == last {
278+
return text[1..bytes.len() - 1].to_string();
279+
}
280+
}
281+
text.to_string()
269282
}
270283

271284
fn handle_library_call(node: &Node, source: &[u8], symbols: &mut FileSymbols) {
@@ -452,6 +465,22 @@ mod tests {
452465
assert_eq!(s.imports[0].source, "dplyr");
453466
}
454467

468+
#[test]
469+
fn source_call_with_mixed_quote_content_preserves_inner_quote() {
470+
// Edge case for the strip_string_quotes fallback: if a grammar
471+
// version drops the `string_content` child, the fallback must strip
472+
// only the outer pair of quotes. `trim_matches` would greedily eat
473+
// both the outer `"` and the inner `'`, returning an empty path.
474+
// Index-based strip leaves the inner `'` intact.
475+
//
476+
// We exercise the fallback indirectly via `source("a'b.R")` —
477+
// current grammars expose `string_content`, so this primarily
478+
// guards against future regressions in the fallback path.
479+
let s = parse_r("source(\"a'b.R\")\n");
480+
assert_eq!(s.imports.len(), 1);
481+
assert_eq!(s.imports[0].source, "a'b.R");
482+
}
483+
455484
#[test]
456485
fn nested_function_assignment_is_recorded() {
457486
// Matches the JS extractor's documented behavior: function

0 commit comments

Comments
 (0)