Skip to content

Commit 1e57feb

Browse files
committed
Refactor
1 parent 139eb5e commit 1e57feb

7 files changed

Lines changed: 127 additions & 76 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ build_rs_cov.profraw
1515
tarpaulin-report.html
1616
.sisyphus/**
1717
.ruff_cache
18+
.omc

crates/cli/src/lib.rs

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -174,29 +174,47 @@ pub async fn run(cli: &Cli) -> Result<bool, Box<dyn std::error::Error + Send + S
174174
}
175175

176176
// 3. Resolve ALL versions concurrently across all manifests (Promise.all pattern)
177+
// Create registries once and share across all manifests of the same kind.
177178
let total_deps: usize = manifest_jobs.iter().map(|j| j.deps.len()).sum();
178179
info!(
179180
manifests = manifest_jobs.len(),
180181
total_deps, "resolving all versions concurrently"
181182
);
182183

184+
let npm_registry = NpmRegistry::new();
185+
let crates_registry = CratesIoRegistry::new();
186+
let pypi_registry = PyPiRegistry::new();
187+
183188
let mut resolve_futures = Vec::new();
184189
for (job_idx, job) in manifest_jobs.iter().enumerate() {
185190
if !job.deps.is_empty() {
191+
let npm = &npm_registry;
192+
let crates_io = &crates_registry;
193+
let pypi = &pypi_registry;
186194
resolve_futures.push(async move {
187-
let resolved = resolve_versions(&job.deps, job.manifest_ref.kind, cli.target).await;
195+
let resolved = match job.manifest_ref.kind {
196+
ManifestKind::PackageJson => {
197+
npm.resolve_batch(&job.deps, cli.target).await
198+
}
199+
ManifestKind::CargoToml => {
200+
crates_io.resolve_batch(&job.deps, cli.target).await
201+
}
202+
ManifestKind::PyProjectToml => {
203+
pypi.resolve_batch(&job.deps, cli.target).await
204+
}
205+
};
188206
(job_idx, resolved)
189207
});
190208
}
191209
}
192210

193211
let resolved_results: Vec<_> = futures::future::join_all(resolve_futures).await;
194212

195-
// Build a map: job_idx -> resolved versions
196-
let mut resolved_map: std::collections::HashMap<usize, ResolvedBatch> =
197-
std::collections::HashMap::new();
213+
// Build a vec: job_idx -> resolved versions (dense indices, no HashMap needed)
214+
let mut resolved_map: Vec<Option<ResolvedBatch>> =
215+
(0..manifest_jobs.len()).map(|_| None).collect();
198216
for (job_idx, resolved) in resolved_results {
199-
resolved_map.insert(job_idx, resolved);
217+
resolved_map[job_idx] = Some(resolved);
200218
}
201219

202220
// 4. Print results and apply updates (sequential — needs ordered output)
@@ -213,7 +231,7 @@ pub async fn run(cli: &Cli) -> Result<bool, Box<dyn std::error::Error + Send + S
213231
continue;
214232
}
215233

216-
let resolved = resolved_map.get(&job_idx).map_or(&[][..], Vec::as_slice);
234+
let resolved = resolved_map[job_idx].as_deref().unwrap_or(&[]);
217235

218236
let success_count = resolved.iter().filter(|(_, r)| r.is_ok()).count();
219237
let fail_count = resolved.len() - success_count;
@@ -282,28 +300,6 @@ fn filter_deps(
282300
.collect()
283301
}
284302

285-
/// Resolve versions for dependencies using the appropriate registry.
286-
async fn resolve_versions(
287-
deps: &[DependencySpec],
288-
kind: ManifestKind,
289-
target: TargetLevel,
290-
) -> Vec<(usize, Result<ResolvedVersion, DcuError>)> {
291-
match kind {
292-
ManifestKind::PackageJson => {
293-
let registry = NpmRegistry::new();
294-
registry.resolve_batch(deps, target).await
295-
}
296-
ManifestKind::CargoToml => {
297-
let registry = CratesIoRegistry::new();
298-
registry.resolve_batch(deps, target).await
299-
}
300-
ManifestKind::PyProjectToml => {
301-
let registry = PyPiRegistry::new();
302-
registry.resolve_batch(deps, target).await
303-
}
304-
}
305-
}
306-
307303
/// Resolved version batch from a registry.
308304
type ResolvedBatch = Vec<(usize, Result<ResolvedVersion, DcuError>)>;
309305

crates/node/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ impl ManifestHandler for NodeHandler {
4141
}
4242

4343
fn apply_updates(&self, text: &str, updates: &[PlannedUpdate]) -> Result<String, DcuError> {
44+
// Use scan_for_updates: skips full JSON parse, only locates deps we need.
4445
let locations =
45-
JsonPatcher::scan_version_locations(text).map_err(|e| DcuError::PatchFailed {
46+
JsonPatcher::scan_for_updates(text, updates).map_err(|e| DcuError::PatchFailed {
4647
path: std::path::PathBuf::from("package.json"),
4748
detail: e.to_string(),
4849
})?;

crates/node/src/patcher.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! finds the exact byte positions of dependency version strings in the original
55
//! text and replaces only those bytes.
66
7-
use dependency_check_updates_core::DependencySection;
7+
use dependency_check_updates_core::{DependencySection, PlannedUpdate};
88

99
use crate::parser::DEPENDENCY_SECTIONS;
1010

@@ -96,6 +96,70 @@ impl JsonPatcher {
9696
Ok(locations)
9797
}
9898

99+
/// Find byte positions of specific dependencies without a full JSON parse.
100+
///
101+
/// This is an optimized path for `apply_updates` where we already know which
102+
/// deps to look for. Scans only the relevant sections and deps, avoiding
103+
/// the cost of deserializing the entire JSON document.
104+
///
105+
/// # Errors
106+
///
107+
/// Returns an error if section positions cannot be found.
108+
pub fn scan_for_updates(
109+
text: &str,
110+
updates: &[PlannedUpdate],
111+
) -> Result<Vec<VersionLocation>, PatchError> {
112+
use std::collections::HashMap;
113+
114+
if updates.is_empty() {
115+
return Ok(Vec::new());
116+
}
117+
118+
// Group updates by section for targeted scanning
119+
let mut by_section: HashMap<DependencySection, Vec<&PlannedUpdate>> = HashMap::new();
120+
for update in updates {
121+
by_section.entry(update.section).or_default().push(update);
122+
}
123+
124+
let mut locations = Vec::with_capacity(updates.len());
125+
126+
for &(section, section_key) in DEPENDENCY_SECTIONS {
127+
let Some(section_updates) = by_section.get(&section) else {
128+
continue;
129+
};
130+
131+
// Find the byte position of this section key in the text
132+
let Some(section_key_pos) = find_json_key_position(text, section_key, 0) else {
133+
continue;
134+
};
135+
136+
let search_from = section_key_pos + section_key.len() + 2;
137+
let Some(obj_start) = find_char_skipping_strings(text, '{', search_from) else {
138+
continue;
139+
};
140+
141+
let Some(obj_end) = find_matching_brace(text, obj_start) else {
142+
continue;
143+
};
144+
145+
// Only scan for deps we need to update
146+
for update in section_updates {
147+
if let Some(loc) = find_dep_value_position(
148+
text,
149+
obj_start,
150+
obj_end,
151+
&update.name,
152+
&update.from,
153+
section,
154+
) {
155+
locations.push(loc);
156+
}
157+
}
158+
}
159+
160+
Ok(locations)
161+
}
162+
99163
/// Apply patches to the original text, replacing version strings.
100164
///
101165
/// Patches are applied back-to-front (highest offset first) so that earlier

crates/node/src/registry.rs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ const MAX_CONCURRENT_REQUESTS: usize = 10;
1616
const REQUEST_TIMEOUT_SECS: u64 = 30;
1717

1818
/// npm registry client for looking up package versions.
19+
#[derive(Clone)]
1920
pub struct NpmRegistry {
2021
client: Client,
2122
semaphore: Arc<Semaphore>,
22-
base_url: String,
23+
base_url: Arc<str>,
2324
}
2425

2526
/// Abbreviated npm package metadata response.
@@ -61,7 +62,7 @@ impl NpmRegistry {
6162
Self {
6263
client,
6364
semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS)),
64-
base_url: base_url.trim_end_matches('/').to_owned(),
65+
base_url: Arc::from(base_url.trim_end_matches('/')),
6566
}
6667
}
6768

@@ -134,16 +135,25 @@ impl NpmRegistry {
134135
let info = self.fetch_package_info(&dep.name).await?;
135136

136137
let latest = info.dist_tags.as_ref().and_then(|dt| dt.latest.clone());
137-
let all_versions = extract_sorted_versions(&info);
138138

139-
trace!(
140-
package = %dep.name,
141-
version_count = all_versions.len(),
142-
latest = ?latest,
143-
"fetched version list"
144-
);
145-
146-
let selected = select_version(&dep.current_req, latest.as_ref(), &all_versions, target);
139+
// Fast path: when target is Latest, skip expensive version parsing/sorting
140+
let selected = if target == TargetLevel::Latest {
141+
trace!(
142+
package = %dep.name,
143+
latest = ?latest,
144+
"fast path: using dist-tags.latest directly"
145+
);
146+
latest.clone()
147+
} else {
148+
let all_versions = extract_sorted_versions(&info);
149+
trace!(
150+
package = %dep.name,
151+
version_count = all_versions.len(),
152+
latest = ?latest,
153+
"fetched version list"
154+
);
155+
select_version(&dep.current_req, latest.as_ref(), &all_versions, target)
156+
};
147157

148158
// Filter out false positives: if the selected version already satisfies
149159
// the current range, there's no manifest change needed.
@@ -172,16 +182,9 @@ impl NpmRegistry {
172182

173183
for (idx, dep) in deps.iter().enumerate() {
174184
let dep = dep.clone();
175-
let client = self.client.clone();
176-
let semaphore = self.semaphore.clone();
177-
let base_url = self.base_url.clone();
185+
let registry = self.clone();
178186

179187
let handle = tokio::spawn(async move {
180-
let registry = NpmRegistry {
181-
client,
182-
semaphore,
183-
base_url,
184-
};
185188
let result = registry.resolve_version(&dep, target).await;
186189
(idx, result)
187190
});
@@ -199,7 +202,7 @@ impl NpmRegistry {
199202
}
200203
}
201204

202-
results.sort_by_key(|(idx, _)| *idx);
205+
results.sort_unstable_by_key(|(idx, _)| *idx);
203206
results
204207
}
205208
}
@@ -221,7 +224,7 @@ fn extract_sorted_versions(info: &NpmPackageInfo) -> Vec<node_semver::Version> {
221224
.filter_map(|v| node_semver::Version::parse(v).ok())
222225
.collect();
223226

224-
parsed.sort();
227+
parsed.sort_unstable();
225228
parsed
226229
}
227230

crates/python/src/registry.rs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,16 @@ const MAX_CONCURRENT_REQUESTS: usize = 10;
1313
const REQUEST_TIMEOUT_SECS: u64 = 30;
1414

1515
/// `PyPI` registry client.
16+
#[derive(Clone)]
1617
pub struct PyPiRegistry {
1718
client: Client,
1819
semaphore: Arc<Semaphore>,
19-
base_url: String,
20+
base_url: Arc<str>,
2021
}
2122

2223
#[derive(Debug, Deserialize)]
2324
struct PyPiResponse {
2425
info: PyPiInfo,
25-
#[allow(dead_code)]
26-
releases: std::collections::HashMap<String, Vec<serde_json::Value>>,
2726
}
2827

2928
#[derive(Debug, Deserialize)]
@@ -57,7 +56,7 @@ impl PyPiRegistry {
5756
Self {
5857
client,
5958
semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS)),
60-
base_url: base_url.trim_end_matches('/').to_owned(),
59+
base_url: Arc::from(base_url.trim_end_matches('/')),
6160
}
6261
}
6362

@@ -140,16 +139,9 @@ impl PyPiRegistry {
140139

141140
for (idx, dep) in deps.iter().enumerate() {
142141
let dep = dep.clone();
143-
let client = self.client.clone();
144-
let semaphore = self.semaphore.clone();
145-
let base_url = self.base_url.clone();
142+
let registry = self.clone();
146143

147144
let handle = tokio::spawn(async move {
148-
let registry = PyPiRegistry {
149-
client,
150-
semaphore,
151-
base_url,
152-
};
153145
let result = registry.resolve_version(&dep, target).await;
154146
(idx, result)
155147
});
@@ -165,7 +157,7 @@ impl PyPiRegistry {
165157
}
166158
}
167159

168-
results.sort_by_key(|(idx, _)| *idx);
160+
results.sort_unstable_by_key(|(idx, _)| *idx);
169161
results
170162
}
171163
}

crates/rust/src/registry.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ const MAX_CONCURRENT_REQUESTS: usize = 10;
1313
const REQUEST_TIMEOUT_SECS: u64 = 30;
1414

1515
/// crates.io registry client.
16+
#[derive(Clone)]
1617
pub struct CratesIoRegistry {
1718
client: Client,
1819
semaphore: Arc<Semaphore>,
19-
base_url: String,
20+
base_url: Arc<str>,
2021
}
2122

2223
#[derive(Debug, Deserialize)]
@@ -56,7 +57,7 @@ impl CratesIoRegistry {
5657
Self {
5758
client,
5859
semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_REQUESTS)),
59-
base_url: base_url.trim_end_matches('/').to_owned(),
60+
base_url: Arc::from(base_url.trim_end_matches('/')),
6061
}
6162
}
6263

@@ -122,7 +123,7 @@ impl CratesIoRegistry {
122123
.filter(|v| !v.yanked)
123124
.filter_map(|v| semver::Version::parse(&v.num).ok())
124125
.collect();
125-
versions.sort();
126+
versions.sort_unstable();
126127

127128
trace!(
128129
crate_name = %dep.name,
@@ -164,16 +165,9 @@ impl CratesIoRegistry {
164165

165166
for (idx, dep) in deps.iter().enumerate() {
166167
let dep = dep.clone();
167-
let client = self.client.clone();
168-
let semaphore = self.semaphore.clone();
169-
let base_url = self.base_url.clone();
168+
let registry = self.clone();
170169

171170
let handle = tokio::spawn(async move {
172-
let registry = CratesIoRegistry {
173-
client,
174-
semaphore,
175-
base_url,
176-
};
177171
let result = registry.resolve_version(&dep, target).await;
178172
(idx, result)
179173
});
@@ -189,7 +183,7 @@ impl CratesIoRegistry {
189183
}
190184
}
191185

192-
results.sort_by_key(|(idx, _)| *idx);
186+
results.sort_unstable_by_key(|(idx, _)| *idx);
193187
results
194188
}
195189
}

0 commit comments

Comments
 (0)