diff --git a/scarb/src/bin/scarb/commands/metadata.rs b/scarb/src/bin/scarb/commands/metadata.rs index fa2dd49dd..59e0fc6db 100644 --- a/scarb/src/bin/scarb/commands/metadata.rs +++ b/scarb/src/bin/scarb/commands/metadata.rs @@ -5,7 +5,8 @@ use toml::de::Error as TomlParseError; use scarb::core::Config; use scarb::core::errors::{ManifestErrorWithSource, ManifestParseError}; use scarb::core::{ - ManifestDiagnosticMessage, ManifestDiagnosticSpan, ManifestMessageKind, ManifestSemanticError, + MachineDiagnostic, MachineDiagnosticKind, MachineDiagnosticSeverity, MachineDiagnosticSpan, + MachineRelatedLocation, ManifestSemanticError, }; use scarb::ops; use scarb_ui::OutputFormat; @@ -42,7 +43,7 @@ pub fn run(args: MetadataArgs, config: &Config) -> Result<()> { } fn emit_manifest_diagnostic(config: &Config, error: &anyhow::Error) { - let (file, message, span, related) = if let Some(sem) = error + let diagnostic = if let Some(sem) = error .chain() .find_map(|c| c.downcast_ref::()) { @@ -59,7 +60,29 @@ fn emit_manifest_diagnostic(config: &Config, error: &anyhow::Error) { }) .unwrap_or_default(); let file = src.map(|src| src.path.to_string()); - (file, sem.to_string(), span, related) + let mut diagnostic = MachineDiagnostic::new( + MachineDiagnosticKind::ManifestDiagnostic, + sem.to_string(), + MachineDiagnosticSeverity::Error, + file.unwrap_or_else(|| "".to_string()), + span.map(|span| MachineDiagnosticSpan { + start: span.start, + end: span.end, + }) + .unwrap_or(MachineDiagnosticSpan { start: 0, end: 0 }), + ); + diagnostic.related = related + .into_iter() + .map(|related| MachineRelatedLocation { + message: related.message, + file: None, + span: MachineDiagnosticSpan { + start: related.span.start, + end: related.span.end, + }, + }) + .collect(); + diagnostic } else if let Some(parse_err) = error .chain() .find_map(|c| c.downcast_ref::()) @@ -76,11 +99,21 @@ fn emit_manifest_diagnostic(config: &Config, error: &anyhow::Error) { }; let span = toml_err .and_then(|e| e.span()) - .map(|s| ManifestDiagnosticSpan { + .map(|s| MachineDiagnosticSpan { start: s.start, end: s.end, }); - (Some(parse_err.path().to_string()), message, span, vec![]) + MachineDiagnostic::new( + MachineDiagnosticKind::ManifestDiagnostic, + message, + MachineDiagnosticSeverity::Error, + parse_err.path().to_string(), + span.map(|span| MachineDiagnosticSpan { + start: span.start, + end: span.end, + }) + .unwrap_or(MachineDiagnosticSpan { start: 0, end: 0 }), + ) } else if let Some(src) = error .chain() .find_map(|c| c.downcast_ref::()) @@ -89,18 +122,16 @@ fn emit_manifest_diagnostic(config: &Config, error: &anyhow::Error) { .source() .map(|err| err.to_string()) .unwrap_or_else(|| error.to_string()); - (Some(src.path.to_string()), message, None, vec![]) + MachineDiagnostic::new( + MachineDiagnosticKind::ManifestDiagnostic, + message, + MachineDiagnosticSeverity::Error, + src.path.to_string(), + MachineDiagnosticSpan { start: 0, end: 0 }, + ) } else { return; }; - config - .ui() - .force_print(MachineMessage(ManifestDiagnosticMessage { - kind: ManifestMessageKind::ManifestDiagnostic, - message, - file, - span, - related, - })); + config.ui().force_print(MachineMessage(diagnostic)); } diff --git a/scarb/src/compiler/helpers.rs b/scarb/src/compiler/helpers.rs index ec8f86cde..20ca8551e 100644 --- a/scarb/src/compiler/helpers.rs +++ b/scarb/src/compiler/helpers.rs @@ -9,7 +9,7 @@ use cairo_lang_compiler::CompilerConfig; use cairo_lang_compiler::diagnostics::DiagnosticsReporter; use cairo_lang_diagnostics::{FormattedDiagnosticEntry, Severity}; use cairo_lang_filesystem::db::FilesGroup; -use cairo_lang_filesystem::ids::CrateId; +use cairo_lang_filesystem::ids::{CrateId, CrateInput}; use itertools::Itertools; use salsa::Database; use serde::Serialize; @@ -41,6 +41,13 @@ impl Write for CountingWriter { } } +pub fn all_crate_inputs(db: &dyn Database) -> Vec { + db.crates() + .iter() + .map(|crate_id| crate_id.long(db).clone().into_crate_input(db)) + .collect_vec() +} + pub fn build_compiler_config<'c, 'db>( db: &'db dyn Database, unit: &CairoCompilationUnit, diff --git a/scarb/src/core/machine_diagnostic.rs b/scarb/src/core/machine_diagnostic.rs new file mode 100644 index 000000000..4e304cec1 --- /dev/null +++ b/scarb/src/core/machine_diagnostic.rs @@ -0,0 +1,83 @@ +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct MachineDiagnostic { + pub kind: MachineDiagnosticKind, + pub message: String, + pub file: String, + pub span: MachineDiagnosticSpan, + pub severity: MachineDiagnosticSeverity, + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub related: Vec, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MachineDiagnosticKind { + Diagnostic, + ManifestDiagnostic, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MachineDiagnosticSeverity { + Error, + Warning, +} + +#[derive(Debug, Clone, Serialize)] +pub struct MachineDiagnosticSpan { + pub start: usize, + pub end: usize, +} + +#[derive(Debug, Clone, Serialize)] +pub struct MachineRelatedLocation { + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub file: Option, + pub span: MachineDiagnosticSpan, +} + +#[derive(Debug, Clone, Serialize)] +pub struct MachineDiagnosticData { + #[serde(skip_serializing_if = "Option::is_none")] + pub span: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub related: Vec, +} + +impl MachineDiagnostic { + pub fn new( + kind: MachineDiagnosticKind, + message: String, + severity: MachineDiagnosticSeverity, + file: String, + span: MachineDiagnosticSpan, + ) -> Self { + Self { + kind, + message, + severity, + code: None, + file, + span, + related: vec![], + } + } + + pub fn severity(&self) -> MachineDiagnosticSeverity { + self.severity + } +} + +impl From> for MachineDiagnosticSpan { + fn from(range: std::ops::Range) -> Self { + Self { + start: range.start, + end: range.end, + } + } +} diff --git a/scarb/src/core/manifest/diagnostic.rs b/scarb/src/core/manifest/diagnostic.rs index d9ccef0ef..32d10c2b1 100644 --- a/scarb/src/core/manifest/diagnostic.rs +++ b/scarb/src/core/manifest/diagnostic.rs @@ -1,65 +1,12 @@ -use crate::core::{PackageName, TargetKind}; +use crate::core::{MachineDiagnosticSpan, PackageName, TargetKind}; use scarb_manifest_schema::FieldPath; -use serde::Serialize; use toml_edit::{Document, Item, Table}; - -/// The serialized shape of a manifest diagnostic emitted in JSON output mode. -/// Both the `metadata` command (for parse/semantic errors) and the workspace -/// loader (for unknown-field warnings) use this type so the JSON output is -/// uniform. -#[derive(Serialize)] -pub struct ManifestDiagnosticMessage { - pub kind: ManifestMessageKind, - pub message: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub file: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub span: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub related: Vec, -} - -#[derive(Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ManifestMessageKind { - ManifestDiagnostic, -} - -#[derive(Debug, Clone, Serialize)] -pub struct ManifestDiagnosticData { - #[serde(skip_serializing_if = "Option::is_none")] - pub span: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub related: Vec, -} - -#[derive(Debug, Clone, Serialize)] -pub struct ManifestRelatedLocation { - pub message: String, - pub span: ManifestDiagnosticSpan, -} - #[derive(Debug, Clone)] pub struct ManifestRelatedAnchor { pub message: String, pub anchor: ManifestDiagnosticAnchor, } -#[derive(Debug, Clone, Serialize)] -pub struct ManifestDiagnosticSpan { - pub start: usize, - pub end: usize, -} - -impl From> for ManifestDiagnosticSpan { - fn from(range: std::ops::Range) -> Self { - Self { - start: range.start, - end: range.end, - } - } -} - #[derive(Debug, Clone)] pub enum ManifestDependencyTable { Dependencies, @@ -207,11 +154,11 @@ impl ManifestDiagnosticAnchor { /// Returns the span of `key` in `table`, preferring the key token span over its value's span. /// Example: in `version = "1.0.0"`, resolves to `version` (or to `"1.0.0"` if the key span is unavailable). -fn key_or_item_span(table: &Table, key: &str) -> Option { +fn key_or_item_span(table: &Table, key: &str) -> Option { let (key, item) = table.get_key_value(key)?; key.span() - .map(ManifestDiagnosticSpan::from) - .or_else(|| item.span().map(ManifestDiagnosticSpan::from)) + .map(MachineDiagnosticSpan::from) + .or_else(|| item.span().map(MachineDiagnosticSpan::from)) } /// Returns the span of an entry in `table` by `key`. @@ -221,22 +168,22 @@ fn table_entry_span( table: &Table, key: &str, field: Option<&'static str>, -) -> Option { +) -> Option { let (entry_key, entry_item) = table.get_key_value(key)?; let Some(field) = field else { return entry_key .span() - .map(ManifestDiagnosticSpan::from) - .or_else(|| entry_item.span().map(ManifestDiagnosticSpan::from)); + .map(MachineDiagnosticSpan::from) + .or_else(|| entry_item.span().map(MachineDiagnosticSpan::from)); }; if let Some(inline_table) = entry_item.as_inline_table() { let (key, item) = inline_table.get_key_value(field)?; return key .span() - .map(ManifestDiagnosticSpan::from) - .or_else(|| item.span().map(ManifestDiagnosticSpan::from)); + .map(MachineDiagnosticSpan::from) + .or_else(|| item.span().map(MachineDiagnosticSpan::from)); } entry_item @@ -261,7 +208,7 @@ fn patch_source_table<'a>(root: &'a Table, source: &str) -> Option<&'a Table> { pub fn resolve_manifest_anchor( source: &str, anchor: &ManifestDiagnosticAnchor, -) -> Option { +) -> Option { let document = Document::parse(source).ok()?; resolve_anchor_in_doc(document.as_table(), anchor) } @@ -270,7 +217,7 @@ pub fn resolve_manifest_anchor( pub fn resolve_anchor_in_doc( root: &Table, anchor: &ManifestDiagnosticAnchor, -) -> Option { +) -> Option { match anchor { ManifestDiagnosticAnchor::PackageField { field } => { table_at_path(root, &["package"]).and_then(|table| key_or_item_span(table, field)) @@ -295,15 +242,15 @@ pub fn resolve_anchor_in_doc( }; field .and_then(|field| key_or_item_span(table, field)) - .or_else(|| table.span().map(ManifestDiagnosticSpan::from)) + .or_else(|| table.span().map(MachineDiagnosticSpan::from)) } ManifestDiagnosticAnchor::PatchRoot => { let patch = table_at_path(root, &["patch"])?; if !patch.is_implicit() { - patch.span().map(ManifestDiagnosticSpan::from) + patch.span().map(MachineDiagnosticSpan::from) } else { patch.iter().find_map(|(_, item)| match item { - Item::Table(table) => table.span().map(ManifestDiagnosticSpan::from), + Item::Table(table) => table.span().map(MachineDiagnosticSpan::from), _ => None, }) } @@ -311,7 +258,7 @@ pub fn resolve_anchor_in_doc( ManifestDiagnosticAnchor::PatchSource { source: patch_source, } => patch_source_table(root, patch_source.as_str()) - .and_then(|t| t.span().map(ManifestDiagnosticSpan::from)), + .and_then(|t| t.span().map(MachineDiagnosticSpan::from)), ManifestDiagnosticAnchor::PatchDependency { source: patch_source, name, @@ -363,7 +310,7 @@ pub fn resolve_anchor_in_doc( return Some(name_span); } - section.span().map(ManifestDiagnosticSpan::from) + section.span().map(MachineDiagnosticSpan::from) } ManifestDiagnosticAnchor::RawTomlPath { path } => { let (key, parent_segments) = path.split_last()?; diff --git a/scarb/src/core/manifest/diagnostic_kinds.rs b/scarb/src/core/manifest/diagnostic_kinds.rs index b5f06e271..957e0439d 100644 --- a/scarb/src/core/manifest/diagnostic_kinds.rs +++ b/scarb/src/core/manifest/diagnostic_kinds.rs @@ -1,16 +1,12 @@ use crate::compiler::ProfileValidationError; -use crate::core::{PackageName, TargetKind}; +use crate::core::{MachineDiagnosticData, MachineRelatedLocation, PackageName, TargetKind}; use camino::Utf8PathBuf; use thiserror::Error; use toml_edit::Table; use url::ParseError as UrlParseError; -use super::ManifestDiagnosticData; use super::diagnostic::resolve_anchor_in_doc; -use super::{ - ManifestDependencyTable, ManifestDiagnosticAnchor, ManifestRelatedAnchor, - ManifestRelatedLocation, -}; +use super::{ManifestDependencyTable, ManifestDiagnosticAnchor, ManifestRelatedAnchor}; /// Typed manifest validation errors that carry semantic anchors for diagnostic span resolution. /// @@ -55,7 +51,7 @@ pub enum ManifestSemanticError { impl ManifestSemanticError { /// Resolves this error's anchor(s) to byte spans using the parsed manifest root table. - pub fn resolve(&self, root: &Table) -> ManifestDiagnosticData { + pub fn resolve(&self, root: &Table) -> MachineDiagnosticData { let span = self .primary_anchor() .and_then(|anchor| resolve_anchor_in_doc(root, &anchor)); @@ -63,14 +59,15 @@ impl ManifestSemanticError { .related_anchors() .into_iter() .filter_map(|r| { - resolve_anchor_in_doc(root, &r.anchor).map(|span| ManifestRelatedLocation { + resolve_anchor_in_doc(root, &r.anchor).map(|span| MachineRelatedLocation { message: r.message, + file: None, span, }) }) .collect(); - ManifestDiagnosticData { span, related } + MachineDiagnosticData { span, related } } fn primary_anchor(&self) -> Option { @@ -502,16 +499,16 @@ mod tests { use super::*; use crate::compiler::ProfileValidationError; - use crate::core::manifest::ManifestDiagnosticSpan; + use crate::core::MachineDiagnosticSpan; /// Parses `toml`, calls `resolve` on `err`, and returns the diagnostic data. - fn resolve_err(err: impl Into, toml: &str) -> ManifestDiagnosticData { + fn resolve_err(err: impl Into, toml: &str) -> MachineDiagnosticData { let doc = toml_edit::Document::parse(toml).expect("valid TOML"); err.into().resolve(doc.as_table()) } /// Slices `source` at `span`, panicking if `span` is `None`. - fn span_text(source: &str, span: Option) -> &str { + fn span_text(source: &str, span: Option) -> &str { let span = span.expect("expected Some span, got None"); &source[span.start..span.end] } diff --git a/scarb/src/core/mod.rs b/scarb/src/core/mod.rs index 05cdd0961..6ad23f948 100644 --- a/scarb/src/core/mod.rs +++ b/scarb/src/core/mod.rs @@ -5,6 +5,7 @@ pub use checksum::*; pub use config::Config; pub use dirs::AppDirs; +pub use machine_diagnostic::*; pub use manifest::*; pub use package::{Package, PackageId, PackageIdInner, PackageInner, PackageName}; pub use resolver::Resolve; @@ -16,6 +17,7 @@ pub(crate) mod config; mod dirs; pub mod errors; pub(crate) mod lockfile; +mod machine_diagnostic; pub(crate) mod manifest; pub(crate) mod package; pub(crate) mod publishing; diff --git a/scarb/src/ops/workspace.rs b/scarb/src/ops/workspace.rs index 645c447cc..bb8d4bcca 100644 --- a/scarb/src/ops/workspace.rs +++ b/scarb/src/ops/workspace.rs @@ -10,16 +10,16 @@ use indoc::indoc; use tracing::trace; use crate::MANIFEST_FILE_NAME; -use crate::core::TomlManifest; use crate::core::config::Config; use crate::core::errors::{ManifestErrorWithSource, ManifestParseError}; -use crate::core::manifest::{ - ManifestDiagnosticAnchor, ManifestDiagnosticMessage, ManifestDiagnosticSpan, - ManifestMessageKind, resolve_manifest_anchor, -}; +use crate::core::manifest::{ManifestDiagnosticAnchor, resolve_manifest_anchor}; use crate::core::package::Package; use crate::core::source::SourceId; use crate::core::workspace::Workspace; +use crate::core::{ + MachineDiagnostic, MachineDiagnosticKind, MachineDiagnosticSeverity, MachineDiagnosticSpan, + TomlManifest, +}; use crate::ops::find_workspace_manifest_path; use scarb_fs_utils as fsx; use scarb_fs_utils::{PathBufUtf8Ext, is_hidden}; @@ -210,15 +210,16 @@ fn warn_unknown_manifest_fields(path: &Utf8Path, source: &str, config: &Config) let span = resolve_manifest_anchor(source, &anchor); if config.ui().output_format() == OutputFormat::Json { + let span = span.unwrap_or(MachineDiagnosticSpan { start: 0, end: 0 }); config .ui() - .force_print(MachineMessage(ManifestDiagnosticMessage { - kind: ManifestMessageKind::ManifestDiagnostic, - message: format!("unknown manifest field `{path_str}`"), - file: Some(path.to_string()), + .force_print(MachineMessage(MachineDiagnostic::new( + MachineDiagnosticKind::ManifestDiagnostic, + format!("unknown manifest field `{path_str}`"), + MachineDiagnosticSeverity::Warning, + path.to_string(), span, - related: vec![], - })); + ))); } else { let location = span .map(|s| format!(" {}", format_span_location(path, source, &s))) @@ -231,7 +232,7 @@ fn warn_unknown_manifest_fields(path: &Utf8Path, source: &str, config: &Config) } /// Converts a byte-offset span to a `(path:line:col)` locator string. -fn format_span_location(path: &Utf8Path, source: &str, span: &ManifestDiagnosticSpan) -> String { +fn format_span_location(path: &Utf8Path, source: &str, span: &MachineDiagnosticSpan) -> String { let (line, col) = byte_offset_to_line_col(source, span.start); format!("({path}:{line}:{col})") } diff --git a/scarb/tests/manifest_warnings.rs b/scarb/tests/manifest_warnings.rs index 305b0a4f4..39e10c834 100644 --- a/scarb/tests/manifest_warnings.rs +++ b/scarb/tests/manifest_warnings.rs @@ -278,8 +278,9 @@ fn json_mode_emits_manifest_diagnostic_for_unknown_top_level_section() { assert!(diag["file"].is_string(), "expected file field"); assert!(diag["span"].is_object(), "expected span field"); assert!( - diag.get("severity").is_none(), - "did not expect severity field" + diag["severity"] == "warning", + "unexpected severity: {}", + diag["severity"] ); } @@ -319,6 +320,7 @@ fn json_mode_emits_manifest_diagnostic_for_unknown_package_field() { ); assert!(diag["file"].is_string(), "expected file field"); assert!(diag["span"].is_object(), "expected span field"); + assert_eq!(diag["severity"], "warning"); } #[test] diff --git a/scarb/tests/metadata.rs b/scarb/tests/metadata.rs index b2d176612..3fcb73aab 100644 --- a/scarb/tests/metadata.rs +++ b/scarb/tests/metadata.rs @@ -150,10 +150,7 @@ fn emits_manifest_diagnostic_ndjson_for_invalid_manifest_in_json_mode() { "#} ); assert!(diagnostic["span"].is_object()); - assert!( - diagnostic.get("severity").is_none(), - "did not expect severity field" - ); + assert_eq!(diagnostic["severity"], "error"); } #[test] @@ -285,8 +282,9 @@ fn emits_manifest_diagnostic_for_semantic_manifest_error_without_span() { diagnostic["message"].as_str().unwrap(), "error inheriting `hello` from workspace root manifest's `workspace.scripts.hello`" ); + assert_eq!(diagnostic["severity"], "error"); assert!(diagnostic.get("code").is_none()); - assert!(diagnostic.get("span").is_none()); + assert_eq!(diagnostic["span"], json!({ "start": 0, "end": 0 })); } #[test]