Skip to content

Commit c8a1572

Browse files
committed
feat: anchor dependency validation errors
Thread ManifestDiagnosticAnchor through dependency parsing so that validation errors (non-Git ref fields, ambiguous git refs, missing source, git/path ambiguity, git/registry ambiguity, workspace dep not found) are emitted as ManifestSemanticError variants with span info. commit-id:44c79848
1 parent d2ca192 commit c8a1572

File tree

2 files changed

+453
-38
lines changed

2 files changed

+453
-38
lines changed

scarb/src/core/manifest/diagnostic_kinds.rs

Lines changed: 353 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
use crate::core::{PackageName, TargetKind};
2+
use camino::Utf8PathBuf;
13
use smol_str::SmolStr;
24
use thiserror::Error;
35
use toml_edit::Document;
6+
use url::ParseError as UrlParseError;
47

58
use super::ManifestDiagnosticData;
69
use super::diagnostic::resolve_anchor_in_doc;
7-
use super::{ManifestDiagnosticAnchor, ManifestRelatedAnchor, ManifestRelatedLocation};
10+
use super::{
11+
ManifestDependencyTable, ManifestDiagnosticAnchor, ManifestRelatedAnchor,
12+
ManifestRelatedLocation,
13+
};
814

915
/// Typed manifest validation errors that carry semantic anchors for diagnostic span resolution.
1016
///
@@ -19,6 +25,32 @@ pub enum ManifestSemanticError {
1925
ProfileInheritanceInvalid(#[from] ProfileInheritanceInvalid),
2026
#[error(transparent)]
2127
ProfileCairoConflict(#[from] ProfileCairoConflict),
28+
#[error(transparent)]
29+
DependencyWorkspaceNotFound(#[from] DependencyWorkspaceNotFound),
30+
#[error(transparent)]
31+
DependencyGitRefWithoutGit(#[from] DependencyGitRefWithoutGit),
32+
#[error(transparent)]
33+
DependencyGitReferenceAmbiguous(#[from] DependencyGitReferenceAmbiguous),
34+
#[error(transparent)]
35+
DependencySourceMissing(#[from] DependencySourceMissing),
36+
#[error(transparent)]
37+
DependencyGitPathAmbiguous(#[from] DependencyGitPathAmbiguous),
38+
#[error(transparent)]
39+
DependencyGitRegistryAmbiguous(#[from] DependencyGitRegistryAmbiguous),
40+
#[error(transparent)]
41+
PatchNotInWorkspaceRoot(#[from] PatchNotInWorkspaceRoot),
42+
#[error(transparent)]
43+
PatchSourceConflict(#[from] PatchSourceConflict),
44+
#[error(transparent)]
45+
PatchSourceInvalidUrl(#[from] PatchSourceInvalidUrl),
46+
#[error(transparent)]
47+
ReadmePathInvalid(#[from] ReadmePathInvalid),
48+
#[error(transparent)]
49+
LicensePathInvalid(#[from] LicensePathInvalid),
50+
#[error(transparent)]
51+
DuplicateDefaultTargetDefinition(#[from] DuplicateDefaultTargetDefinition),
52+
#[error(transparent)]
53+
DuplicateNamedTargetDefinition(#[from] DuplicateNamedTargetDefinition),
2254
}
2355

2456
impl ManifestSemanticError {
@@ -54,12 +86,29 @@ impl ManifestSemanticError {
5486
Self::ProfileNameInvalid(e) => Some(e.primary_anchor()),
5587
Self::ProfileInheritanceInvalid(e) => Some(e.primary_anchor()),
5688
Self::ProfileCairoConflict(e) => Some(e.primary_anchor()),
89+
Self::DependencyWorkspaceNotFound(e) => Some(e.primary_anchor()),
90+
Self::DependencyGitRefWithoutGit(e) => Some(e.primary_anchor()),
91+
Self::DependencyGitReferenceAmbiguous(e) => Some(e.primary_anchor()),
92+
Self::DependencySourceMissing(e) => Some(e.primary_anchor()),
93+
Self::DependencyGitPathAmbiguous(e) => Some(e.primary_anchor()),
94+
Self::DependencyGitRegistryAmbiguous(e) => Some(e.primary_anchor()),
95+
Self::PatchNotInWorkspaceRoot(e) => Some(e.primary_anchor()),
96+
Self::PatchSourceConflict(e) => Some(e.primary_anchor()),
97+
Self::PatchSourceInvalidUrl(e) => Some(e.primary_anchor()),
98+
Self::ReadmePathInvalid(e) => e.primary_anchor(),
99+
Self::LicensePathInvalid(e) => e.primary_anchor(),
100+
Self::DuplicateDefaultTargetDefinition(e) => Some(e.primary_anchor()),
101+
Self::DuplicateNamedTargetDefinition(e) => Some(e.primary_anchor()),
57102
}
58103
}
59104

60105
fn related_anchors(&self) -> Vec<ManifestRelatedAnchor> {
61106
match self {
62107
Self::ProfileCairoConflict(e) => e.related_anchors(),
108+
Self::DependencyGitReferenceAmbiguous(e) => e.related_anchors(),
109+
Self::DependencyGitPathAmbiguous(e) => e.related_anchors(),
110+
Self::DependencyGitRegistryAmbiguous(e) => e.related_anchors(),
111+
Self::PatchSourceConflict(e) => e.related_anchors(),
63112
_ => vec![],
64113
}
65114
}
@@ -135,3 +184,306 @@ impl ProfileCairoConflict {
135184
}]
136185
}
137186
}
187+
188+
// ── Dependency errors ─────────────────────────────────────────────────────────
189+
190+
#[derive(Debug, Clone, Error)]
191+
#[error("dependency `{name}` not found in workspace")]
192+
pub struct DependencyWorkspaceNotFound {
193+
pub name: PackageName,
194+
pub table: ManifestDependencyTable,
195+
}
196+
197+
impl DependencyWorkspaceNotFound {
198+
pub fn new(name: PackageName, table: ManifestDependencyTable) -> Self {
199+
Self { name, table }
200+
}
201+
202+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
203+
ManifestDiagnosticAnchor::dependency(self.table.clone(), self.name.clone())
204+
}
205+
}
206+
207+
#[derive(Debug, Clone, Error)]
208+
#[error("dependency ({name}) is non-Git, but provides `branch`, `tag` or `rev`")]
209+
pub struct DependencyGitRefWithoutGit {
210+
pub name: PackageName,
211+
pub anchor: ManifestDiagnosticAnchor,
212+
pub field: &'static str,
213+
}
214+
215+
impl DependencyGitRefWithoutGit {
216+
pub fn new(name: PackageName, anchor: ManifestDiagnosticAnchor, field: &'static str) -> Self {
217+
Self {
218+
name,
219+
anchor,
220+
field,
221+
}
222+
}
223+
224+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
225+
self.anchor.clone().with_field(self.field)
226+
}
227+
}
228+
229+
#[derive(Debug, Clone, Error)]
230+
#[error(
231+
"dependency ({name}) specification is ambiguous, only one of `branch`, `tag` or `rev` is allowed"
232+
)]
233+
pub struct DependencyGitReferenceAmbiguous {
234+
pub name: PackageName,
235+
pub anchor: ManifestDiagnosticAnchor,
236+
pub fields: Vec<&'static str>,
237+
}
238+
239+
impl DependencyGitReferenceAmbiguous {
240+
pub fn new(
241+
name: PackageName,
242+
anchor: ManifestDiagnosticAnchor,
243+
fields: Vec<&'static str>,
244+
) -> Self {
245+
Self {
246+
name,
247+
anchor,
248+
fields,
249+
}
250+
}
251+
252+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
253+
self.anchor
254+
.clone()
255+
.with_field(self.fields.first().copied().unwrap_or("branch"))
256+
}
257+
258+
fn related_anchors(&self) -> Vec<ManifestRelatedAnchor> {
259+
self.fields
260+
.iter()
261+
.skip(1)
262+
.map(|field| ManifestRelatedAnchor {
263+
message: "conflicting Git reference".to_string(),
264+
anchor: self.anchor.clone().with_field(field),
265+
})
266+
.collect()
267+
}
268+
}
269+
270+
#[derive(Debug, Clone, Error)]
271+
#[error(
272+
"dependency ({name}) must be specified providing a local path, Git repository, or version to use"
273+
)]
274+
pub struct DependencySourceMissing {
275+
pub name: PackageName,
276+
pub anchor: ManifestDiagnosticAnchor,
277+
}
278+
279+
impl DependencySourceMissing {
280+
pub fn new(name: PackageName, anchor: ManifestDiagnosticAnchor) -> Self {
281+
Self { name, anchor }
282+
}
283+
284+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
285+
self.anchor.clone()
286+
}
287+
}
288+
289+
#[derive(Debug, Clone, Error)]
290+
#[error("dependency ({name}) specification is ambiguous, only one of `git` or `path` is allowed")]
291+
pub struct DependencyGitPathAmbiguous {
292+
pub name: PackageName,
293+
pub anchor: ManifestDiagnosticAnchor,
294+
}
295+
296+
impl DependencyGitPathAmbiguous {
297+
pub fn new(name: PackageName, anchor: ManifestDiagnosticAnchor) -> Self {
298+
Self { name, anchor }
299+
}
300+
301+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
302+
self.anchor.clone().with_field("git")
303+
}
304+
305+
fn related_anchors(&self) -> Vec<ManifestRelatedAnchor> {
306+
vec![ManifestRelatedAnchor {
307+
message: "conflicts with this field".to_string(),
308+
anchor: self.anchor.clone().with_field("path"),
309+
}]
310+
}
311+
}
312+
313+
#[derive(Debug, Clone, Error)]
314+
#[error(
315+
"dependency ({name}) specification is ambiguous, only one of `git` or `registry` is allowed"
316+
)]
317+
pub struct DependencyGitRegistryAmbiguous {
318+
pub name: PackageName,
319+
pub anchor: ManifestDiagnosticAnchor,
320+
}
321+
322+
impl DependencyGitRegistryAmbiguous {
323+
pub fn new(name: PackageName, anchor: ManifestDiagnosticAnchor) -> Self {
324+
Self { name, anchor }
325+
}
326+
327+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
328+
self.anchor.clone().with_field("git")
329+
}
330+
331+
fn related_anchors(&self) -> Vec<ManifestRelatedAnchor> {
332+
vec![ManifestRelatedAnchor {
333+
message: "conflicts with this field".to_string(),
334+
anchor: self.anchor.clone().with_field("registry"),
335+
}]
336+
}
337+
}
338+
339+
// ── Patch errors ──────────────────────────────────────────────────────────────
340+
341+
#[derive(Debug, Clone, Error)]
342+
#[error(
343+
"the `[patch]` section can only be defined in the workspace root manifests\n\
344+
section found in manifest: `{manifest_path}`\n\
345+
workspace root manifest: `{workspace_manifest_path}`"
346+
)]
347+
pub struct PatchNotInWorkspaceRoot {
348+
pub manifest_path: Utf8PathBuf,
349+
pub workspace_manifest_path: Utf8PathBuf,
350+
}
351+
352+
impl PatchNotInWorkspaceRoot {
353+
pub fn new(manifest_path: Utf8PathBuf, workspace_manifest_path: Utf8PathBuf) -> Self {
354+
Self {
355+
manifest_path,
356+
workspace_manifest_path,
357+
}
358+
}
359+
360+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
361+
ManifestDiagnosticAnchor::patch_root()
362+
}
363+
}
364+
365+
#[derive(Debug, Clone, Error)]
366+
#[error("the `[patch]` section cannot specify both `{source_a}` and `{source_b}`")]
367+
pub struct PatchSourceConflict {
368+
pub source_a: SmolStr,
369+
pub source_b: SmolStr,
370+
}
371+
372+
impl PatchSourceConflict {
373+
pub fn new(source_a: impl Into<SmolStr>, source_b: impl Into<SmolStr>) -> Self {
374+
Self {
375+
source_a: source_a.into(),
376+
source_b: source_b.into(),
377+
}
378+
}
379+
380+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
381+
ManifestDiagnosticAnchor::patch_source(self.source_a.clone())
382+
}
383+
384+
fn related_anchors(&self) -> Vec<ManifestRelatedAnchor> {
385+
vec![ManifestRelatedAnchor {
386+
message: "conflicts with this source".to_string(),
387+
anchor: ManifestDiagnosticAnchor::patch_source(self.source_b.clone()),
388+
}]
389+
}
390+
}
391+
392+
#[derive(Debug, Clone, Error)]
393+
#[error("failed to parse `{raw_source}` as patch source url")]
394+
pub struct PatchSourceInvalidUrl {
395+
pub raw_source: SmolStr,
396+
#[source]
397+
pub cause: UrlParseError,
398+
}
399+
400+
impl PatchSourceInvalidUrl {
401+
pub fn new(raw_source: impl Into<SmolStr>, cause: UrlParseError) -> Self {
402+
Self {
403+
raw_source: raw_source.into(),
404+
cause,
405+
}
406+
}
407+
408+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
409+
ManifestDiagnosticAnchor::patch_source(self.raw_source.clone())
410+
}
411+
}
412+
413+
// ── File-path errors ──────────────────────────────────────────────────────────
414+
415+
#[derive(Debug, Clone, Error)]
416+
#[error("failed to find readme at {path}")]
417+
pub struct ReadmePathInvalid {
418+
pub path: Utf8PathBuf,
419+
pub anchor: Option<ManifestDiagnosticAnchor>,
420+
}
421+
422+
impl ReadmePathInvalid {
423+
pub fn new(path: Utf8PathBuf, anchor: Option<ManifestDiagnosticAnchor>) -> Self {
424+
Self { path, anchor }
425+
}
426+
427+
fn primary_anchor(&self) -> Option<ManifestDiagnosticAnchor> {
428+
self.anchor.clone()
429+
}
430+
}
431+
432+
#[derive(Debug, Clone, Error)]
433+
#[error("failed to find license at {path}")]
434+
pub struct LicensePathInvalid {
435+
pub path: Utf8PathBuf,
436+
pub anchor: Option<ManifestDiagnosticAnchor>,
437+
}
438+
439+
impl LicensePathInvalid {
440+
pub fn new(path: Utf8PathBuf, anchor: Option<ManifestDiagnosticAnchor>) -> Self {
441+
Self { path, anchor }
442+
}
443+
444+
fn primary_anchor(&self) -> Option<ManifestDiagnosticAnchor> {
445+
self.anchor.clone()
446+
}
447+
}
448+
449+
// ── Target errors ─────────────────────────────────────────────────────────────
450+
451+
#[derive(Debug, Clone, Error)]
452+
#[error(
453+
"manifest contains duplicate target definitions `{kind}`, \
454+
consider explicitly naming targets with the `name` field"
455+
)]
456+
pub struct DuplicateDefaultTargetDefinition {
457+
pub kind: TargetKind,
458+
pub name: SmolStr,
459+
}
460+
461+
impl DuplicateDefaultTargetDefinition {
462+
pub fn new(kind: TargetKind, name: SmolStr) -> Self {
463+
Self { kind, name }
464+
}
465+
466+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
467+
ManifestDiagnosticAnchor::target(self.kind.clone(), Some(self.name.clone()))
468+
}
469+
}
470+
471+
#[derive(Debug, Clone, Error)]
472+
#[error(
473+
"manifest contains duplicate target definitions `{kind} ({name})`, \
474+
use different target names to resolve the conflict"
475+
)]
476+
pub struct DuplicateNamedTargetDefinition {
477+
pub kind: TargetKind,
478+
pub name: SmolStr,
479+
}
480+
481+
impl DuplicateNamedTargetDefinition {
482+
pub fn new(kind: TargetKind, name: SmolStr) -> Self {
483+
Self { kind, name }
484+
}
485+
486+
fn primary_anchor(&self) -> ManifestDiagnosticAnchor {
487+
ManifestDiagnosticAnchor::target(self.kind.clone(), Some(self.name.clone()))
488+
}
489+
}

0 commit comments

Comments
 (0)