diff --git a/Cargo.lock b/Cargo.lock index 8496553b61..5e5cbeda91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.8.3" @@ -11,6 +26,7 @@ dependencies = [ "cfg-if", "getrandom", "once_cell", + "serde", "version_check", ] @@ -154,7 +170,7 @@ dependencies = [ "polling", "rustix 0.37.25", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] @@ -231,6 +247,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.21.4" @@ -249,6 +280,21 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -294,6 +340,12 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "bytecount" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad152d03a2c813c80bb94fedbf3a3f02b28f793e39e7c214c8a0bcc196343de7" + [[package]] name = "byteorder" version = "1.4.3" @@ -584,6 +636,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "diff" version = "0.1.13" @@ -698,6 +756,16 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -761,6 +829,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "futures" version = "0.3.28" @@ -890,10 +968,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "glob" version = "0.3.1" @@ -912,6 +998,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1003,12 +1108,70 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1135,6 +1298,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + [[package]] name = "is-terminal" version = "0.4.9" @@ -1146,6 +1315,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + [[package]] name = "itertools" version = "0.11.0" @@ -1170,6 +1348,36 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" +dependencies = [ + "ahash", + "anyhow", + "base64", + "bytecount", + "clap 4.4.3", + "fancy-regex", + "fraction", + "getrandom", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time", + "url", + "uuid", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -1321,12 +1529,38 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -1406,6 +1640,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + [[package]] name = "num-complex" version = "0.4.4" @@ -1468,6 +1708,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -1658,6 +1907,7 @@ dependencies = [ "codespan-reporting", "plc_ast", "plc_source", + "serde_json", ] [[package]] @@ -1691,6 +1941,8 @@ dependencies = [ "encoding_rs", "encoding_rs_io", "glob", + "insta", + "jsonschema", "plc_diagnostics", "plc_source", "regex", @@ -1926,6 +2178,41 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rsa" version = "0.9.2" @@ -1948,6 +2235,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustix" version = "0.37.25" @@ -2094,6 +2387,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serial_test" version = "2.0.0" @@ -2188,6 +2493,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -2490,6 +2805,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.8.0" @@ -2538,6 +2874,33 @@ dependencies = [ "syn 2.0.33", ] +[[package]] +name = "time" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +dependencies = [ + "deranged", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2553,6 +2916,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2 0.5.4", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.11" @@ -2562,6 +2955,12 @@ dependencies = [ "serde", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.37" @@ -2595,6 +2994,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + [[package]] name = "typenum" version = "1.16.0" @@ -2669,6 +3074,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" + [[package]] name = "value-bag" version = "1.4.1" @@ -2693,6 +3104,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2965,6 +3385,16 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "xshell" version = "0.2.5" diff --git a/compiler/plc_diagnostics/Cargo.toml b/compiler/plc_diagnostics/Cargo.toml index c32229c019..c61a8f98e4 100644 --- a/compiler/plc_diagnostics/Cargo.toml +++ b/compiler/plc_diagnostics/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" codespan-reporting = "0.11.1" plc_ast = { path = "../plc_ast" } plc_source = { path = "../plc_source" } +serde_json = "1" diff --git a/compiler/plc_diagnostics/src/diagnostics.rs b/compiler/plc_diagnostics/src/diagnostics.rs index 712fc510f1..25783c20f3 100644 --- a/compiler/plc_diagnostics/src/diagnostics.rs +++ b/compiler/plc_diagnostics/src/diagnostics.rs @@ -1,7 +1,11 @@ use std::{error::Error, fmt::Display, ops::Range}; use plc_ast::ast::{AstNode, DataTypeDeclaration, DiagnosticInfo, PouType}; -use plc_source::source_location::SourceLocation; + +use plc_source::{ + source_location::{SourceLocation, SourceLocationFactory}, + BuildDescriptionSource, +}; use crate::errno::ErrNo; @@ -16,6 +20,13 @@ pub enum Diagnostic { CombinedDiagnostic { message: String, inner_diagnostics: Vec, err_no: ErrNo }, } +#[derive(Debug)] +pub struct SerdeError { + message: String, + line: usize, + column: usize, +} + impl Display for Diagnostic { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.get_type(), self.get_message())?; @@ -784,6 +795,39 @@ impl Diagnostic { err_no: ErrNo::cfc__unnamed_control, } } + + pub fn invalid_build_description_file(message: String, location: Option) -> Diagnostic { + let range = if let Some(range) = location { vec![range] } else { vec![SourceLocation::internal()] }; + Diagnostic::SemanticError { message, range, err_no: ErrNo::plc_json__invalid } + } +} + +// Necessary in-between step to convert serde error to diagnostics, since there is +// a conflicting `From` impl for `Diagnostic` +impl From for SerdeError { + fn from(value: serde_json::Error) -> Self { + let line = value.line(); + let column = value.column(); + + // remove line, column from message + let message = value.to_string(); + let message = if let Some(pos) = message.find("at line") { + message.chars().take(pos).collect() + } else { + message + }; + + SerdeError { message, line, column } + } +} + +impl SerdeError { + pub fn into_diagnostic(self, src: &BuildDescriptionSource) -> Diagnostic { + let factory = SourceLocationFactory::for_source(src); + let range = factory.create_range_to_end_of_line(self.line, self.column); + + Diagnostic::invalid_build_description_file(self.message, Some(range)) + } } #[cfg(test)] diff --git a/compiler/plc_diagnostics/src/errno.rs b/compiler/plc_diagnostics/src/errno.rs index 6aba6dd7d2..fb3feb2855 100644 --- a/compiler/plc_diagnostics/src/errno.rs +++ b/compiler/plc_diagnostics/src/errno.rs @@ -96,6 +96,9 @@ pub enum ErrNo { cfc__cyclic_connection, cfc__no_associated_connector, cfc__unnamed_control, + + // Project description file + plc_json__invalid, } impl Display for ErrNo { diff --git a/compiler/plc_project/Cargo.toml b/compiler/plc_project/Cargo.toml index cf1e27e1f0..ad14786c05 100644 --- a/compiler/plc_project/Cargo.toml +++ b/compiler/plc_project/Cargo.toml @@ -12,6 +12,10 @@ source_code = { path = "../plc_source/", package = "plc_source" } serde = { version = "1.0", features = ["derive"] } serde_json = "1" regex = "1" +jsonschema = "0.17" encoding_rs.workspace = true encoding_rs_io.workspace = true glob = "*" + +[dev-dependencies] +insta = "1.31.0" diff --git a/compiler/plc_project/schema/plc-json.schema b/compiler/plc_project/schema/plc-json.schema new file mode 100644 index 0000000000..a4f4ceb791 --- /dev/null +++ b/compiler/plc_project/schema/plc-json.schema @@ -0,0 +1,77 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Schema for plc.json", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "format_version": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "compile_type": { + "type": "string" + }, + "output": { + "type": "string" + }, + "libraries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "package": { + "type": "string" + }, + "include_path": { + "type": "array", + "items": { + "type": "string" + } + }, + "architectures": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "path", + "package", + "include_path" + ] + } + }, + "package_commands": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "name", + "files", + "compile_type" + ] + } \ No newline at end of file diff --git a/compiler/plc_project/src/build_config.rs b/compiler/plc_project/src/build_config.rs index eb414fbc2a..3f1b469f7d 100644 --- a/compiler/plc_project/src/build_config.rs +++ b/compiler/plc_project/src/build_config.rs @@ -1,8 +1,12 @@ +use jsonschema::JSONSchema; use plc::Target; use plc_diagnostics::diagnostics::Diagnostic; +use plc_diagnostics::diagnostics::SerdeError; use regex::Captures; use regex::Regex; use serde::{Deserialize, Serialize}; +use serde_json::json; +use source_code::BuildDescriptionSource; use std::env; use std::fs; use std::path::Path; @@ -34,36 +38,76 @@ pub enum LinkageInfo { } #[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct ProjectConfig { pub name: String, pub files: Vec, #[serde(default)] pub compile_type: FormatOption, #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] pub output: Option, #[serde(default)] pub libraries: Vec, #[serde(default)] pub package_commands: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "format-version")] + pub format_version: Option, } impl ProjectConfig { - /// Retuns a project from the given string (in json format) + /// Returns a project from the given json-source /// All environment variables (marked with `$VAR_NAME`) that can be resovled at this time are resolved before the conversion - pub fn try_parse(content: &str) -> Result { + pub fn try_parse(source: BuildDescriptionSource) -> Result { + let content = source.source.as_str(); let content = resolve_environment_variables(content)?; - serde_json::from_str(&content).map_err(Into::into) + let config: ProjectConfig = serde_json::from_str(&content).map_err(|err| { + let err = SerdeError::from(err); + err.into_diagnostic(&source) + })?; + config.validate()?; + + Ok(config) } pub(crate) fn from_file(config: &Path) -> Result { //read from file let content = fs::read_to_string(config)?; + let content = BuildDescriptionSource::new(content, config); //convert file to Object - let project = ProjectConfig::try_parse(&content)?; + let project = ProjectConfig::try_parse(content)?; Ok(project) } + + fn validate(&self) -> Result<(), Diagnostic> { + let schema_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(Path::new("schema/plc-json.schema")); + let schema = fs::read_to_string(schema_path).map_err(Diagnostic::from)?; + let schema_obj = serde_json::from_str(&schema).expect("A valid schema"); + let compiled = JSONSchema::compile(&schema_obj).expect("A valid schema"); + let instance = json!(self); + compiled.validate(&instance).map_err(|errors| { + let mut message = String::from("plc.json could not be validated due to the following errors:\n"); + for err in errors { + let prefix = match err.kind { + jsonschema::error::ValidationErrorKind::MinItems { .. } => { + err.instance_path.to_string().replace('/', "") + } + _ => "".into(), + }; + message.push_str(&format!("{prefix}{err}\n")); + } + + // XXX: jsonschema does not provide error messages with location info + Diagnostic::invalid_build_description_file(message, None) + }) + } } //TODO: I don't think this belongs here @@ -86,11 +130,141 @@ mod tests { use std::{env, vec}; use crate::build_config::default_targets; + use insta::assert_snapshot; use plc::output::FormatOption; use super::LibraryConfig; use super::{LinkageInfo, ProjectConfig}; + const SIMPLE_PROGRAM: &str = r#" + { + "name": "MyProject", + "files" : [ + "simple_program.st" + ], + "compile_type" : "Shared", + "output": "proj.so", + "libraries" : [ + { + "name" : "copy", + "path" : "libs/", + "package" : "Copy", + "include_path" : [ + "simple_program.st" + ] + }, + { + "name" : "nocopy", + "path" : "libs/", + "package" : "System", + "include_path" : [ + "simple_program.st" + ] + }, + { + "name" : "static", + "path" : "libs/", + "package" : "Static", + "include_path" : [ + "simple_program.st" + ] + }, + { + "name" : "withTargets", + "path" : "libs/", + "package" : "Static", + "include_path" : [ + "simple_program.st" + ], + "architectures": ["myArch", "myArch2"] + } + ] + } +"#; + + const ADDITIONAL_UNKNOWN_PROPERTIES: &str = r#" + { + "name": "MyProject", + "files" : [ + "file.st" + ], + "compile_type" : "Shared", + "output": "proj.so", + "additional_field" : "should give an error" + } +"#; + + const NO_FILES_SPECIFIED: &str = r#" + { + "name": "MyProject", + "files" : [], + "compile_type" : "Shared", + "output": "proj.so" + } + "#; + + const INVALID_ENUM_VARIANTS: &str = r#" + { + "name": "MyProject", + "files" : [ + "simple_program.st" + ], + "compile_type" : "Interpreted", + "output": "proj.so", + "libraries" : [ + { + "name" : "static", + "path" : "libs/", + "package" : "Opened", + "include_path" : [ + "simple_program.st" + ] + } + ] + } +"#; + + const MISSING_REQUIRED_LIBRARY_PROPERTIES: &str = r#" + { + "name": "MyProject", + "files" : [ + "simple_program.st" + ], + "compile_type" : "Shared", + "output": "proj.so", + "libraries" : [ + { + "name" : "static", + "package" : "Static", + "include_path" : [ + "simple_program.st" + ] + } + ] + } +"#; + + const MISSING_REQUIRED_PROPERTIES: &str = r#" + { + "files" : [ + "simple_program.st" + ] + } + "#; + + const OPTIONAL_PROPERTIES: &str = r#" + { + "version": "0.1", + "format-version": "0.2", + "name": "MyProject", + "files" : [ + "file.st" + ], + "compile_type" : "Shared", + "output": "proj.so" + } +"#; + #[test] fn check_build_struct_from_file() { let test_project = ProjectConfig { @@ -129,55 +303,10 @@ mod tests { }, ], package_commands: vec![], + version: None, + format_version: None, }; - let proj = ProjectConfig::try_parse( - r#" - { - "name": "MyProject", - "files" : [ - "simple_program.st" - ], - "compile_type" : "Shared", - "output": "proj.so", - "libraries" : [ - { - "name" : "copy", - "path" : "libs/", - "package" : "Copy", - "include_path" : [ - "simple_program.st" - ] - }, - { - "name" : "nocopy", - "path" : "libs/", - "package" : "System", - "include_path" : [ - "simple_program.st" - ] - }, - { - "name" : "static", - "path" : "libs/", - "package" : "Static", - "include_path" : [ - "simple_program.st" - ] - }, - { - "name" : "withTargets", - "path" : "libs/", - "package" : "Static", - "include_path" : [ - "simple_program.st" - ], - "architectures": ["myArch", "myArch2"] - } - ] - } - "#, - ) - .unwrap(); + let proj = ProjectConfig::try_parse(SIMPLE_PROGRAM.into()).unwrap(); assert_eq!(test_project.name, proj.name); assert_eq!(test_project.files, proj.files); @@ -205,12 +334,73 @@ mod tests { "name" : "$test_var", "files" : [ "simple_program.st" - ] + ], + "compile_type" : "Shared", + "output": "proj.so" } - "#, + "# + .into(), ) .unwrap(); assert_eq!("test_value", &proj.name); } + + #[test] + fn valid_json_validates_without_errors() { + let cfg = ProjectConfig::try_parse(SIMPLE_PROGRAM.into()); + + assert!(cfg.is_ok()) + } + + #[test] + fn json_with_additional_fields_reports_unexpected_fields() { + let Err(diag) = ProjectConfig::try_parse(ADDITIONAL_UNKNOWN_PROPERTIES.into()) else { + panic!("expected errors") + }; + + assert_snapshot!(diag.to_string()) + } + + #[test] + fn json_with_invalid_enum_variants_reports_error() { + let Err(diag) = ProjectConfig::try_parse(INVALID_ENUM_VARIANTS.into()) else { + panic!("expected errors") + }; + + assert_snapshot!(diag.to_string()) + } + + #[test] + fn json_with_missing_required_properties_reports_error() { + // missing name and compile_type + //XXX: only the first error found is reported by both serde and jsonschema + let Err(diag) = ProjectConfig::try_parse(MISSING_REQUIRED_PROPERTIES.into()) else { + panic!("expected errors") + }; + assert_snapshot!(diag.to_string()); + + // missing library path + let Err(diag) = ProjectConfig::try_parse(MISSING_REQUIRED_LIBRARY_PROPERTIES.into()) else { + panic!("expected errors") + }; + assert_snapshot!(diag.to_string()) + } + + #[test] + fn json_with_empty_files_array_reports_error() { + let Err(diag) = ProjectConfig::try_parse(NO_FILES_SPECIFIED.into()) else { + panic!("expected errors") + }; + + assert_snapshot!(diag.to_string()) + } + + #[test] + fn json_with_optional_properties_is_valid() { + match ProjectConfig::try_parse(OPTIONAL_PROPERTIES.into()) { + Ok(cfg) => assert_snapshot!(&format!("{:#?}", cfg)), + Err(err) => panic!("expected ProjectConfig to be OK, got \n {err}"), + }; + } } diff --git a/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_additional_fields_reports_unexpected_fields.snap b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_additional_fields_reports_unexpected_fields.snap new file mode 100644 index 0000000000..03c3ae9557 --- /dev/null +++ b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_additional_fields_reports_unexpected_fields.snap @@ -0,0 +1,5 @@ +--- +source: compiler/plc_project/src/build_config.rs +expression: diag.to_string() +--- +plc_json__invalid: unknown field `additional_field`, expected one of `name`, `files`, `compile_type`, `output`, `libraries`, `package_commands`, `version`, `format-version`, `format_version` at: :9:27:{9:27-9:215}: diff --git a/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_empty_files_array_reports_error.snap b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_empty_files_array_reports_error.snap new file mode 100644 index 0000000000..423f7c98eb --- /dev/null +++ b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_empty_files_array_reports_error.snap @@ -0,0 +1,7 @@ +--- +source: compiler/plc_project/src/build_config.rs +expression: diag.to_string() +--- +plc_json__invalid: plc.json could not be validated due to the following errors: +files[] has less than 1 item + diff --git a/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_invalid_enum_variants_reports_error.snap b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_invalid_enum_variants_reports_error.snap new file mode 100644 index 0000000000..6be1942573 --- /dev/null +++ b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_invalid_enum_variants_reports_error.snap @@ -0,0 +1,5 @@ +--- +source: compiler/plc_project/src/build_config.rs +expression: diag.to_string() +--- +plc_json__invalid: unknown variant `Interpreted`, expected one of `Object`, `Static`, `PIC`, `Shared`, `NoPIC`, `Relocatable`, `Bitcode`, `IR` at: :7:38:{7:38-7:168}: diff --git a/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_missing_required_properties_reports_error-2.snap b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_missing_required_properties_reports_error-2.snap new file mode 100644 index 0000000000..557cfea9bc --- /dev/null +++ b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_missing_required_properties_reports_error-2.snap @@ -0,0 +1,5 @@ +--- +source: compiler/plc_project/src/build_config.rs +expression: diag.to_string() +--- +plc_json__invalid: missing field `path` at: :16:13:{16:13-16:391}: diff --git a/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_missing_required_properties_reports_error.snap b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_missing_required_properties_reports_error.snap new file mode 100644 index 0000000000..764f4e1fb2 --- /dev/null +++ b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_missing_required_properties_reports_error.snap @@ -0,0 +1,5 @@ +--- +source: compiler/plc_project/src/build_config.rs +expression: diag.to_string() +--- +plc_json__invalid: missing field `name` at: :6:9:{6:9-6:100}: diff --git a/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_optional_properties_is_valid.snap b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_optional_properties_is_valid.snap new file mode 100644 index 0000000000..1a9ab0b266 --- /dev/null +++ b/compiler/plc_project/src/snapshots/plc_project__build_config__tests__json_with_optional_properties_is_valid.snap @@ -0,0 +1,22 @@ +--- +source: compiler/plc_project/src/build_config.rs +expression: "&format!(\"{:#?}\", cfg)" +--- +ProjectConfig { + name: "MyProject", + files: [ + "file.st", + ], + compile_type: Shared, + output: Some( + "proj.so", + ), + libraries: [], + package_commands: [], + version: Some( + "0.1", + ), + format_version: Some( + "0.2", + ), +} diff --git a/compiler/plc_source/src/lib.rs b/compiler/plc_source/src/lib.rs index b317667034..f2b07e159d 100644 --- a/compiler/plc_source/src/lib.rs +++ b/compiler/plc_source/src/lib.rs @@ -75,6 +75,8 @@ impl SourceContainer for SourceCode { } } +pub type BuildDescriptionSource = SourceCode; + ///Extension trait to create sources with names from strs, used in tests pub trait SourceCodeFactory { fn create_source(self, path: impl Into) -> SourceCode; diff --git a/compiler/plc_source/src/source_location.rs b/compiler/plc_source/src/source_location.rs index 4f7aba9d86..55ac6875fa 100644 --- a/compiler/plc_source/src/source_location.rs +++ b/compiler/plc_source/src/source_location.rs @@ -42,6 +42,12 @@ impl SourceLocationFactory { pub fn create_file_only_location(&self) -> SourceLocation { SourceLocation { span: CodeSpan::None, file: self.file } } + + pub fn create_range_to_end_of_line(&self, line: usize, column: usize) -> SourceLocation { + let start = TextLocation::new(line, column, 0); + let end = TextLocation::new(line, self.newlines.get_end_of_line(line), 0); + SourceLocation { span: CodeSpan::Range(start..end), file: self.file } + } } /// The location of a certain element in a text file @@ -323,6 +329,12 @@ impl NewLines { offset } } + + /// + /// returns the 0 based column of end-of-line character for the given line + pub fn get_end_of_line(&self, line: usize) -> usize { + self.line_breaks.get(line).copied().unwrap_or_default() + } } #[cfg(test)] diff --git a/tests/integration/data/json/build_cc_linker.json b/tests/integration/data/json/build_cc_linker.json index fe671b222b..17af4a4fd0 100644 --- a/tests/integration/data/json/build_cc_linker.json +++ b/tests/integration/data/json/build_cc_linker.json @@ -1,9 +1,7 @@ { - "name" : "cc_proj", - "files" : [ + "name": "cc_proj", + "files": [ "simple_program.st" ], - "compile_type" : "Shared", - "optimization" : "Default", - "error_format": "Rich" -} + "compile_type": "Shared" +} \ No newline at end of file diff --git a/tests/integration/data/json/build_clang_windows.json b/tests/integration/data/json/build_clang_windows.json index 13bcf8b833..d45121baea 100644 --- a/tests/integration/data/json/build_clang_windows.json +++ b/tests/integration/data/json/build_clang_windows.json @@ -1,19 +1,17 @@ { - "name" : "clang_proj", - "files" : [ + "name": "clang_proj", + "files": [ "program_with_variables.st" ], - "compile_type" : "Shared", - "optimization" : "Default", - "error_format": "Rich", - "libraries" : [ + "compile_type": "Shared", + "libraries": [ { - "name" : "test", - "path" : ".", - "package" : "System", - "include_path" : [ + "name": "test", + "path": ".", + "package": "System", + "include_path": [ "program_with_expressions.st" ] } ] -} +} \ No newline at end of file diff --git a/tests/integration/data/json/build_to_temp.json b/tests/integration/data/json/build_to_temp.json index edd408ed9a..5907686b2b 100644 --- a/tests/integration/data/json/build_to_temp.json +++ b/tests/integration/data/json/build_to_temp.json @@ -1,25 +1,26 @@ { - "name" : "proj", - "files" : [ + "name": "proj", + "files": [ "simple_program.st" ], - "compile_type" : "Shared", - "libraries" : [ + "compile_type": "Shared", + "libraries": [ { - "name" : "copy", - "path" : "libs/", - "package" : "Copy", - "include_path" : [ + "name": "copy", + "path": "libs/", + "package": "Copy", + "include_path": [ "program_with_expressions.st" ] }, { - "name" : "nocopy", - "path" : "libs/", - "package" : "System", - "include_path" : [ + "name": "nocopy", + "path": "libs/", + "package": "System", + "include_path": [ "program_with_variables.st" ] } - ] -} + ], + "output": "proj.so" +} \ No newline at end of file diff --git a/tests/integration/data/json/build_without_sysroot.json b/tests/integration/data/json/build_without_sysroot.json index 47c3406eb2..492c9dfd5b 100644 --- a/tests/integration/data/json/build_without_sysroot.json +++ b/tests/integration/data/json/build_without_sysroot.json @@ -1,7 +1,8 @@ { - "name" : "proj", - "files" : [ + "name": "proj", + "files": [ "simple_program.st" ], - "compile_type" : "Shared" -} + "compile_type": "Shared", + "output": "proj.so" +} \ No newline at end of file diff --git a/tests/integration/data/json/multi_target_and_sysroot.json b/tests/integration/data/json/multi_target_and_sysroot.json index 47c3406eb2..492c9dfd5b 100644 --- a/tests/integration/data/json/multi_target_and_sysroot.json +++ b/tests/integration/data/json/multi_target_and_sysroot.json @@ -1,7 +1,8 @@ { - "name" : "proj", - "files" : [ + "name": "proj", + "files": [ "simple_program.st" ], - "compile_type" : "Shared" -} + "compile_type": "Shared", + "output": "proj.so" +} \ No newline at end of file diff --git a/tests/integration/data/json/separate_build_and_lib.json b/tests/integration/data/json/separate_build_and_lib.json index e49af31e71..03713a4236 100644 --- a/tests/integration/data/json/separate_build_and_lib.json +++ b/tests/integration/data/json/separate_build_and_lib.json @@ -1,25 +1,26 @@ { - "name" : "proj", - "files" : [ + "name": "proj", + "files": [ "simple_program.st" ], - "compile_type" : "Shared", - "libraries" : [ + "compile_type": "Shared", + "libraries": [ { - "name" : "copy2", - "path" : "libs/", - "package" : "Copy", - "include_path" : [ + "name": "copy2", + "path": "libs/", + "package": "Copy", + "include_path": [ "program_with_expressions.st" ] }, { - "name" : "nocopy", - "path" : "libs/", - "package" : "System", - "include_path" : [ + "name": "nocopy", + "path": "libs/", + "package": "System", + "include_path": [ "program_with_variables.st" ] } - ] -} + ], + "output": "proj.so" +} \ No newline at end of file