Skip to content
Merged
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
49 changes: 34 additions & 15 deletions crates/socket-patch-core/src/formats/gem/gemspec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
//! caller cannot know what bundler would lock.

use super::gemfile::{code_of, leading_quoted, split_args};
use super::manifest::code_lines;

/// The `gemspec` files of one directory: `(file name, text)`, the text
/// `None` when the file exists but could not be read. `Err` when the
Expand Down Expand Up @@ -95,9 +96,8 @@ pub(crate) struct GemspecDirective {
/// `path:` / `name:`, a `glob:`, an absolute or escaping path).
pub(crate) fn gemspec_directives(gemfile: &str) -> Result<Vec<GemspecDirective>, String> {
let mut out = Vec::new();
for (line, text) in gemfile.split('\n').enumerate() {
let trimmed = text.trim_start();
let Some(rest) = trimmed.strip_prefix("gemspec") else {
for (line, text, code) in code_lines(gemfile) {
let Some(rest) = code.trim_start().strip_prefix("gemspec") else {
continue;
};
// `gemspec`, `gemspec path: "x"`, `gemspec(path: "x")`; never
Expand Down Expand Up @@ -234,15 +234,11 @@ pub(crate) fn dev_dependency_requirements(
gem: &str,
) -> Result<Option<Vec<String>>, String> {
let mut found: Option<Vec<String>> = None;
for (i, text) in gemspec.split('\n').enumerate() {
let trimmed = text.trim();
if trimmed.starts_with('#') {
continue;
}
let Some(at) = trimmed.find("add_development_dependency") else {
for (i, text, code) in code_lines(gemspec) {
let Some(at) = code.find("add_development_dependency") else {
continue;
};
let after = &trimmed[at + "add_development_dependency".len()..];
let after = &code[at + "add_development_dependency".len()..];
// Every literal spelling [`literal`] reads: quoted, or `%q` with any
// of its bracket pairs.
let mentions = [
Expand All @@ -255,7 +251,7 @@ pub(crate) fn dev_dependency_requirements(
]
.iter()
.any(|(open, close)| after.contains(&format!("{open}{gem}{close}")));
let unreadable = || format!("line {}: `{trimmed}`", i + 1);
let unreadable = || format!("line {}: `{}`", i + 1, text.trim());
let Some(reqs) = read_dev_call(after) else {
if mentions {
return Err(unreadable());
Expand Down Expand Up @@ -331,10 +327,8 @@ pub(crate) fn locked_exact_requirement(
if !bundler_merges_dev_dependencies(lock) {
return Ok(exact);
}
let Some(decl) = gemfile.split('\n').position(|line| {
let trimmed = line.trim_start();
!trimmed.starts_with('#')
&& crate::vendor::gem::gem_declaration_any(trimmed).is_some_and(|d| d.name == name)
let Some((decl, _, _)) = code_lines(gemfile).find(|(_, _, code)| {
crate::vendor::gem::gem_declaration_any(code.trim_start()).is_some_and(|d| d.name == name)
}) else {
return Ok(exact);
};
Expand Down Expand Up @@ -553,6 +547,31 @@ mod tests {
);
}

#[test]
fn begin_blocks_heredocs_and_end_data_are_not_code() {
let gemfile =
"=begin\ngemspec path: \"old\"\n=end\nX = <<~RUBY\n gemspec path: ROOT\nRUBY\n\
gemspec path: \"core\"\n__END__\ngemspec glob: \"*\"\n";
assert_eq!(
gemspec_directives(gemfile),
Ok(vec![GemspecDirective {
line: 6,
dir: "core".into(),
name: None
}])
);
let spec = "=begin\n s.add_development_dependency \"vuln-gem\", \"~> 2\"\n=end\n\
DOC = <<-EOS\n s.add_development_dependency \"vuln-gem\", VERSION\n EOS\n";
assert_eq!(dev_dependency_requirements(spec, "vuln-gem"), Ok(None));
// A `gem` line inside a `=begin` block is not the declaration the
// directive must precede.
let gemfile =
"=begin\ngem \"vuln-gem\", \"0.9\"\n=end\ngemspec\ngem \"vuln-gem\", \"1.0.0\"\n";
let got =
locked_exact_requirement(gemfile, LOCK_27, "vuln-gem", "1.0.0", &mut one(GEMSPEC));
assert_eq!(got.as_deref(), Ok(">= 0, = 1.0.0"));
}

#[test]
fn an_unreadable_dev_dependency_or_gemspec_is_an_error() {
let gemfile = "gemspec\ngem \"vuln-gem\", \"1.0.0\"\n";
Expand Down
32 changes: 18 additions & 14 deletions crates/socket-patch-core/src/formats/gem/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ impl GemfileLockfile {
/// ([`eval_gemfile_paths`]).
pub fn gemfile_lockfile(contents: &str) -> Option<GemfileLockfile> {
let mut found: Option<GemfileLockfile> = None;
for_each_code_line(contents, |line, code| {
for (_, line, code) in code_lines(contents) {
let lead = code.len() - code.trim_start().len();
for at in ident_positions(code, "lockfile") {
let this = if at == lead {
Expand All @@ -412,7 +412,7 @@ pub fn gemfile_lockfile(contents: &str) -> Option<GemfileLockfile> {
Some(prev) => prev.merge(this),
});
}
});
}
found
}

Expand All @@ -421,15 +421,15 @@ pub fn gemfile_lockfile(contents: &str) -> Option<GemfileLockfile> {
/// A computed path is left out.
pub fn eval_gemfile_paths(contents: &str) -> Vec<String> {
let mut out = Vec::new();
for_each_code_line(contents, |_, code| {
for (_, _, code) in code_lines(contents) {
for at in ident_positions(code, "eval_gemfile") {
let arg = code[at + "eval_gemfile".len()..].trim_start();
let arg = arg.strip_prefix('(').map(str::trim_start).unwrap_or(arg);
if let Some((value, _)) = plain_literal(arg) {
out.push(value.to_string());
}
}
});
}
out
}

Expand Down Expand Up @@ -465,16 +465,19 @@ fn plain_literal(s: &str) -> Option<(&str, &str)> {
(!value.contains('\\') && !interpolated).then_some((value, rest))
}

/// Run `f(line, code)` over each line of Ruby source that holds code:
/// `code` is the line before any `#` comment. Skips `=begin`/`=end`
/// blocks and heredoc bodies, and stops at `__END__`. A leading BOM is
/// dropped, as Ruby does.
pub(crate) fn for_each_code_line<'a>(contents: &'a str, mut f: impl FnMut(&'a str, &'a str)) {
/// Each line of Ruby source that holds code: `(0-based line index, line,
/// code)`, `code` being the line before any `#` comment. Skips
/// `=begin`/`=end` blocks and heredoc bodies, and stops at `__END__`. A
/// leading BOM is dropped, as Ruby does.
pub(crate) fn code_lines(contents: &str) -> impl Iterator<Item = (usize, &str, &str)> {
use crate::formats::gem::gemfile::code_of;
let contents = crate::formats::text::strip_bom(contents);
let mut lines = crate::formats::text::strip_bom(contents)
.lines()
.enumerate();
let mut in_comment_block = false;
let mut heredocs: Vec<String> = Vec::new();
for line in contents.lines() {
std::iter::from_fn(move || loop {
let (i, line) = lines.next()?;
if let Some(id) = heredocs.first() {
if line.trim() == id {
heredocs.remove(0);
Expand All @@ -490,12 +493,13 @@ pub(crate) fn for_each_code_line<'a>(contents: &'a str, mut f: impl FnMut(&'a st
continue;
}
if line.trim_end() == "__END__" {
break;
return None;
}
let code = code_of(line).unwrap_or(line);
heredocs.extend(heredoc_ids(code));
f(line, code);
}
return Some((i, line, code));
})
.fuse()
}

/// The byte offsets in `code` where `name` appears as a bare identifier
Expand Down
5 changes: 3 additions & 2 deletions crates/socket-patch-core/src/vex/product.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,9 @@ fn xml_element_texts<const N: usize>(xml: &str, paths: [&[&str]; N]) -> [Option<
/// heredoc bodies and anything after `__END__` are not code, and the last
/// assignment wins (#1573).
fn parse_gemspec(content: &str, stem: &str) -> Option<String> {
let mut lines: Vec<&str> = Vec::new();
crate::formats::gem::manifest::for_each_code_line(content, |_, c| lines.push(c));
let lines: Vec<&str> = crate::formats::gem::manifest::code_lines(content)
.map(|(_, _, code)| code)
.collect();
// The `spec` of `Gem::Specification.new do |spec|`, when the file names
// it, so a helper's `ext.name = …` elsewhere can't pass for the spec's.
let receiver = lines
Expand Down
Loading