diff --git a/Cargo.lock b/Cargo.lock index ff6bf97..f90258e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.0" @@ -35,6 +41,21 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "annotate-snippets" version = "0.11.4" @@ -106,6 +127,17 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arca" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f915ddd863ef73f11c10c75170e86db1d4f539689bc6bfb9ce25d6528d6fe83" +dependencies = [ + "clean-path", + "path-slash", + "radix_trie", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -148,6 +180,21 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[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 = "2.6.0" @@ -262,7 +309,7 @@ dependencies = [ "humantime", "ignore", "im-rc", - "indexmap", + "indexmap 2.6.0", "itertools", "jobserver", "lazycell", @@ -433,6 +480,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + [[package]] name = "clap" version = "4.5.21" @@ -473,6 +533,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +[[package]] +name = "clean-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa6b4b263a5d737e9bf6b7c09b72c41a5480aec4d7219af827f6564e950b6a5" + [[package]] name = "clru" version = "0.6.2" @@ -520,6 +586,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent_lru" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feb5cb312f774e8a24540e27206db4e890f7d488563671d24a16389cf4c2e4e" +dependencies = [ + "once_cell", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -877,6 +952,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "equivalent" version = "1.0.1" @@ -915,6 +996,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + [[package]] name = "faster-hex" version = "0.9.0" @@ -969,7 +1061,7 @@ checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -1839,6 +1931,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1912,6 +2010,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -2087,6 +2208,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.6.0" @@ -2095,8 +2227,15 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown 0.15.2", + "serde", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "is_executable" version = "1.0.4" @@ -2170,6 +2309,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-strip-comments" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b271732a960335e715b6b2ae66a086f115c74eb97360e996d2bd809bfc063bba" +dependencies = [ + "memchr", +] + [[package]] name = "kstring" version = "2.0.2" @@ -2358,6 +2506,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -2433,6 +2590,15 @@ dependencies = [ "libloading", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nom" version = "7.1.3" @@ -2449,6 +2615,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" +[[package]] +name = "normalize-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" + [[package]] name = "normpath" version = "1.3.0" @@ -2756,6 +2928,26 @@ dependencies = [ "unicode-id-start", ] +[[package]] +name = "oxc_resolver" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da33ec82d0f4770f4b5c120d6a1d8e45de2d99ae2672a7ee6bd29092ada945f2" +dependencies = [ + "cfg-if", + "dashmap", + "indexmap 2.6.0", + "json-strip-comments", + "once_cell", + "pnp", + "rustc-hash", + "serde", + "serde_json", + "simdutf8", + "thiserror 1.0.69", + "tracing", +] + [[package]] name = "oxc_semantic" version = "0.38.0" @@ -2763,7 +2955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa39eef07f48c3182b703d474c8a0a8bcc8ca788339bc9238d79b925ea435745" dependencies = [ "assert-unchecked", - "indexmap", + "indexmap 2.6.0", "itertools", "oxc_allocator", "oxc_ast", @@ -2867,6 +3059,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2895,7 +3093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.6.0", ] [[package]] @@ -2962,6 +3160,26 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "pnp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46770cee76a618023fea15411d0449dd066dc232cc17e4562f154da215f27af7" +dependencies = [ + "arca", + "byteorder", + "concurrent_lru", + "fancy-regex", + "lazy_static", + "miniz_oxide 0.7.4", + "pathdiff", + "regex", + "serde", + "serde_json", + "serde_with", + "thiserror 1.0.69", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3013,6 +3231,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -3310,6 +3538,7 @@ version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ + "indexmap 2.6.0", "itoa", "memchr", "ryu", @@ -3325,6 +3554,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3390,6 +3649,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "0.3.11" @@ -3675,7 +3940,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", @@ -3978,6 +4243,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4166,6 +4440,10 @@ dependencies = [ [[package]] name = "wyw_processor" version = "0.1.0" +dependencies = [ + "oxc", + "wyw_traverse", +] [[package]] name = "wyw_sample_processor" @@ -4177,6 +4455,24 @@ dependencies = [ "wyw_processor", ] +[[package]] +name = "wyw_shaker" +version = "0.1.0" +dependencies = [ + "indoc", + "itertools", + "normalize-path", + "oxc", + "oxc_index", + "oxc_resolver", + "oxc_semantic", + "regex", + "serde_json", + "tempfile", + "wyw_processor", + "wyw_traverse", +] + [[package]] name = "wyw_traverse" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8d58bd1..ca26f83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,19 @@ rust-version = "1.83.0" [workspace.dependencies] wyw_macros = { version = "0.1.0", path = "crates/wyw_macros" } wyw_processor = { version = "0.1.0", path = "crates/wyw_processor" } +wyw_shaker = { version = "0.1.0", path = "crates/wyw_shaker" } wyw_traverse = { version = "0.1.0", path = "crates/wyw_traverse" } +indoc = { version= "2.0.5" } +itertools = { version = "0.13.0" } napi = { version = "2.12.2", default-features = false, features = ["napi4"] } napi-build = "2.0.1" napi-derive = "2.12.2" +normalize-path = { version = "0.2.1" } oxc = "0.38.0" +oxc_index = "1.0.0" oxc_resolver = { version = "2.1.1", features = ["package_json_raw_json_api", "pnp", "yarn_pnp"] } oxc_semantic = "0.38.0" +regex = "1.11.1" +serde_json = "1.0.133" +tempfile = "3.14.0" diff --git a/crates/wyw_processor/Cargo.toml b/crates/wyw_processor/Cargo.toml index 0047ad5..1f9ca9e 100644 --- a/crates/wyw_processor/Cargo.toml +++ b/crates/wyw_processor/Cargo.toml @@ -6,3 +6,6 @@ edition.workspace = true rust-version.workspace = true [dependencies] +wyw_traverse = { workspace = true } + +oxc = { workspace = true } diff --git a/crates/wyw_processor/src/lib.rs b/crates/wyw_processor/src/lib.rs index 108311b..5d6f163 100644 --- a/crates/wyw_processor/src/lib.rs +++ b/crates/wyw_processor/src/lib.rs @@ -1,5 +1,16 @@ -pub trait Processor { +use std::fmt::Debug; + +pub mod params; +pub mod replacement_value; + +pub trait Processor: Debug { fn id(&self) -> &str; fn transform(&self) -> String; } + +pub struct ProcessorTarget { + pub specifier: String, + pub source: String, + pub processor: Box, +} diff --git a/crates/wyw_processor/src/params.rs b/crates/wyw_processor/src/params.rs new file mode 100644 index 0000000..561995e --- /dev/null +++ b/crates/wyw_processor/src/params.rs @@ -0,0 +1,101 @@ +use crate::Processor; +use oxc::span::{Atom, Span}; +use std::borrow::Cow; +use std::fmt::Debug; +use std::path::PathBuf; +use wyw_traverse::local_identifier::LocalIdentifier; + +#[derive(Debug)] +pub enum ConstValue<'a> { + BigInt(Span, Atom<'a>), + Boolean(Span, bool), + Null(Span), + Number(Span, f64), + String(Span, Atom<'a>), + Undefined(Span), +} + +pub enum ExpressionValue<'a> { + ConstValue(ConstValue<'a>), + Function(Span), + Ident(Span, Atom<'a>), + Source(Span), + TemplateValue { + cooked: Option>, + raw: Atom<'a>, + span: Span, + }, +} + +impl<'a> Debug for ExpressionValue<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExpressionValue::ConstValue(value) => write!(f, "{:?}", value), + ExpressionValue::Function(span) => write!(f, "Function({:?})", span), + ExpressionValue::Ident(span, ident) => { + write!(f, "Ident({:?}..{:?}, {:?})", span.start, span.end, ident) + } + ExpressionValue::Source(span) => write!(f, "Source({:?}..{:?})", span.start, span.end), + ExpressionValue::TemplateValue { span, raw, .. } => { + write!( + f, + "TemplateValue({:?}..{:?}, {:?})", + span.start, span.end, raw + ) + } + } + } +} + +pub enum Param<'a> { + Callee(Span, LocalIdentifier<'a>), + Call(Span, Vec>), + Member(Span, Atom<'a>), + Template(Span, Vec>), +} + +impl<'a> Debug for Param<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Param::Callee(span, ident) => { + write!(f, "Callee({:?}..{:?}, {:?})", span.start, span.end, ident) + } + Param::Call(span, args) => write!(f, "Call({:?}..{:?}, {:?}))", span.start, span.end, args), + Param::Member(span, prop) => { + write!(f, "Member({:?}..{:?}, {:?}))", span.start, span.end, prop) + } + Param::Template(span, exprs) => write!( + f, + "Template({:?}..{:?}, {:?}))", + span.start, span.end, exprs + ), + } + } +} + +pub struct ProcessorParams<'a> { + pub idx: usize, + pub display_name: Cow<'a, str>, + pub params: Vec>, + pub root: &'a PathBuf, + pub filename: &'a PathBuf, +} + +#[derive(Debug)] +pub struct ProcessorCall<'a> { + pub span: Span, + pub processor: Box, + pub params: ProcessorParams<'a>, +} + +impl<'a> Debug for ProcessorParams<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ProcessorParams") + .field("idx", &self.idx) + .field("display_name", &self.display_name) + .field("params", &self.params) + .finish() + } +} + +pub type ProcessorCalls<'a> = Vec>; diff --git a/crates/wyw_processor/src/replacement_value.rs b/crates/wyw_processor/src/replacement_value.rs new file mode 100644 index 0000000..d2d9c26 --- /dev/null +++ b/crates/wyw_processor/src/replacement_value.rs @@ -0,0 +1,19 @@ +use oxc::span::Span; + +#[derive(Debug, PartialEq)] +pub enum ReplacementValue { + Del, + Span(Span), + Str(String), + Undefined, +} + +impl ReplacementValue { + pub fn from_string(s: &str) -> Self { + if s == "undefined" { + Self::Undefined + } else { + Self::Str(s.to_string()) + } + } +} diff --git a/crates/wyw_sample_processor/src/lib.rs b/crates/wyw_sample_processor/src/lib.rs index c3c8dd3..08b8786 100644 --- a/crates/wyw_sample_processor/src/lib.rs +++ b/crates/wyw_sample_processor/src/lib.rs @@ -27,7 +27,7 @@ pub struct TransformOptions { // TODO: this should be generated by a macro #[napi] -pub fn transform(filename: String, source_code: String, options: TransformOptions) -> String { +pub fn transform(_: String, _: String, _: TransformOptions) -> String { let processors = HashMap::from([(TransformTargetProcessors::SampleTag, SampleTagProcessor {})]); // TODO this is a stub implementation, will be moved to wyw-in-js-transform @@ -39,6 +39,7 @@ pub fn transform(filename: String, source_code: String, options: TransformOption // TODO: this is an actual impl, will stay in this crate +#[derive(Debug)] struct SampleTagProcessor {} impl Processor for SampleTagProcessor { diff --git a/crates/wyw_shaker/Cargo.toml b/crates/wyw_shaker/Cargo.toml new file mode 100644 index 0000000..94c182e --- /dev/null +++ b/crates/wyw_shaker/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wyw_shaker" +version = "0.1.0" + +edition.workspace = true +rust-version.workspace = true + +[dependencies] +wyw_processor = { workspace = true } +wyw_traverse = { workspace = true } + +indoc = { workspace = true } +itertools = { workspace = true } +normalize-path = { workspace = true } +oxc = { workspace = true } +oxc_index = { workspace = true } +oxc_resolver = { workspace = true } +oxc_semantic = { workspace = true } +regex = { workspace = true } +serde_json = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/wyw_shaker/src/declaration_context.rs b/crates/wyw_shaker/src/declaration_context.rs new file mode 100644 index 0000000..84f85e3 --- /dev/null +++ b/crates/wyw_shaker/src/declaration_context.rs @@ -0,0 +1,238 @@ +use oxc::ast::ast::*; +use wyw_traverse::symbol::Symbol; +use wyw_traverse::{Ancestor, AnyNode}; + +#[derive(Clone, Debug)] +pub enum PathPart<'a> { + Index(usize), + Member(Atom<'a>), +} + +#[derive(Debug)] +pub struct DeclaredIdent<'a> { + pub symbol: Symbol, + pub from: Vec>, +} + +#[derive(Debug)] +pub enum DeclarationContext<'a> { + None, + List(Vec>), +} + +fn get_property_key<'a, 'b>(prop: &'b BindingProperty<'a>) -> Option<&'b Atom<'a>> { + match &prop.key { + PropertyKey::StaticIdentifier(ident) => Some(&ident.name), + + _ => None, + } +} + +fn unfold<'a>( + pattern: &BindingPatternKind<'a>, + stack: &mut Vec>, +) -> Vec> { + match pattern { + BindingPatternKind::BindingIdentifier(ident) => { + let symbol_id = ident.symbol_id.get().expect("Expected a symbol id"); + vec![DeclaredIdent { + symbol: Symbol::new_with_name(ident.name.to_string(), symbol_id, ident.span), + from: stack.clone(), + }] + } + + BindingPatternKind::ArrayPattern(array) => array + .elements + .iter() + .enumerate() + .filter_map(|(idx, elem)| match elem { + Some(elem) => { + stack.push(PathPart::Index(idx)); + let res = unfold(&elem.kind, stack); + stack.pop(); + Some(res) + } + + None => None, + }) + .flatten() + .collect(), + + BindingPatternKind::AssignmentPattern(assigment) => unfold(&assigment.left.kind, stack), + + BindingPatternKind::ObjectPattern(object) => { + let mut res = vec![]; + + for prop in &object.properties { + let key = get_property_key(prop); + if key.is_none() { + // FIXME: It's okay if we will not try to use this context later + continue; + } + stack.push(PathPart::Member(key.unwrap().clone())); + res.extend(unfold(&prop.value.kind, stack)); + stack.pop(); + } + + if let Some(ident) = &object.rest { + res.extend(unfold(&ident.argument.kind, stack)); + } + + res + } + } +} + +impl<'a> DeclarationContext<'a> { + pub fn from(node: &VariableDeclarator<'a>) -> Self { + match &node.id.kind { + BindingPatternKind::BindingIdentifier(ident) => DeclarationContext::List({ + let symbol_id = ident.symbol_id.get().expect("Expected a symbol id"); + let decl = ident.span; + + vec![DeclaredIdent { + symbol: Symbol::new_with_name(ident.name.to_string(), symbol_id, decl), + from: vec![], + }] + }), + + pattern => DeclarationContext::List(unfold(pattern, &mut vec![])), + } + } + + pub fn from_ancestors(ancestors: &[Ancestor<'a>]) -> Self { + let decl = ancestors.iter().rev().find_map(|ancestor| match ancestor { + Ancestor::Field(AnyNode::VariableDeclarator(decl), _) => Some(decl), + _ => None, + }); + + match decl { + None => DeclarationContext::None, + Some(decl) => DeclarationContext::from(decl), + } + } + + // pub(crate) fn get_declaring_symbol(&self) -> Option { + // match &self { + // DeclarationContext::List(list) if list.len() == 1 => Some(list[0].symbol.clone()), + // _ => None, + // } + // } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxc::allocator::Allocator; + use oxc::parser::{ParseOptions, Parser}; + use oxc_semantic::SemanticBuilder; + use std::path::Path; + + fn run(pattern: &str) -> String { + let allocator = Allocator::default(); + + let source_text = format!("const {} = obj;", pattern); + + let path = Path::new("test.js"); + let source_type = SourceType::from_path(path).unwrap(); + + let parser_ret = Parser::new(&allocator, &source_text, source_type) + .with_options(ParseOptions { + parse_regular_expression: true, + ..ParseOptions::default() + }) + .parse(); + + assert!(parser_ret.errors.is_empty()); + + let program = allocator.alloc(parser_ret.program); + + SemanticBuilder::new() + .build_module_record(path, program) + .with_check_syntax_error(true) + .build(program); + + if let Statement::VariableDeclaration(decl) = &program.body[0] { + return DeclarationContext::from(&decl.declarations[0]).to_debug_string(); + } + + panic!("Expected a variable declaration statement"); + } + + impl<'a> DeclarationContext<'a> { + fn to_debug_string(self) -> String { + match self { + DeclarationContext::None => "".to_string(), + + DeclarationContext::List(list) => list + .iter() + .map(|ident| { + let from = ident + .from + .iter() + .map(|part| match part { + PathPart::Member(ident) => ident.to_string(), + PathPart::Index(idx) => idx.to_string(), + }) + .collect::>() + .join("."); + + format!("{}:{}", ident.symbol.name, from) + }) + .collect::>() + .join(", "), + } + } + } + + #[test] + fn test_simple_ident() { + // const a = obj; + assert_eq!(run("a"), "a:"); + } + + #[test] + fn test_simple_array() { + // const [a, b] = obj; + assert_eq!(run("[a, b]"), "a:0, b:1"); + } + + #[test] + fn test_nested_array() { + // const [a, [b, c]] = obj; + assert_eq!(run("[a, [b, c]]"), "a:0, b:1.0, c:1.1"); + } + + #[test] + fn test_simple_object() { + // const {a, b} = obj; + assert_eq!(run("{a, b}"), "a:a, b:b"); + } + + #[test] + fn test_nested_object() { + // const {a, b: {b, c}} = obj; + assert_eq!(run("{a, b: {b, c}}"), "a:a, b:b.b, c:b.c"); + } + + #[test] + fn test_rest_object() { + // const {a, ...rest} = obj; + assert_eq!(run("{a, ...rest}"), "a:a, rest:"); + } + + #[test] + fn test_with_default() { + // const {a = 1} = obj; + assert_eq!(run("{a = 1}"), "a:a"); + } + + #[test] + fn test_mixed() { + // const {a, b = 1, c: [c, d] = [], ...rest} = obj; + assert_eq!( + run("{a, b = 1, c: [c, d] = [], ...rest}"), + "a:a, b:b, c:c.0, d:c.1, rest:" + ); + } +} diff --git a/crates/wyw_shaker/src/default_resolver.rs b/crates/wyw_shaker/src/default_resolver.rs new file mode 100644 index 0000000..9ad5105 --- /dev/null +++ b/crates/wyw_shaker/src/default_resolver.rs @@ -0,0 +1,122 @@ +use std::path::PathBuf; + +pub const EXTENSIONS: [&str; 6] = [".ts", ".tsx", ".mts", ".js", ".mjs", ".cjs"]; + +fn find_tsconfig(start: PathBuf) -> Option { + let mut path = start; + + loop { + let tsconfig = path.join("tsconfig.json"); + + if tsconfig.exists() { + return Some(tsconfig); + } + + if !path.pop() { + break; + } + } + + None +} + +pub fn create_resolver(file_path_buf: &PathBuf) -> oxc_resolver::Resolver { + // TODO: this should be cached on directory level + let tsconfig_path = find_tsconfig(file_path_buf.parent().unwrap().to_path_buf()); + let tsconfig = { + if let Some(tsconfig_path) = tsconfig_path { + Some(oxc_resolver::TsconfigOptions { + config_file: tsconfig_path, + references: oxc_resolver::TsconfigReferences::Auto, + }) + } else { + None + } + }; + + let resolver_options = oxc_resolver::ResolveOptions { + tsconfig, + condition_names: vec!["import".into(), "require".into(), "node".into()], + extensions: EXTENSIONS.map(|ext| ext.to_string()).into(), + main_fields: vec!["module".into(), "main".into()], + ..oxc_resolver::ResolveOptions::default() + }; + + let resolver = oxc_resolver::Resolver::new(resolver_options); + + resolver +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + use std::{ + fs::{self, File}, + io::Write, + }; + + use tempfile::tempdir; + + #[test] + fn test_resolve_existing_module_ts() { + let dir = tempdir().unwrap(); + let dir_path = dir.path(); + + let ts_module_path = dir_path.join("existing_module.ts"); + File::create(&ts_module_path).unwrap(); + + let js_module_path = dir_path.join("existing_module.js"); + File::create(&js_module_path).unwrap(); + + let resolver = create_resolver(&dir_path.to_path_buf()); + let resolved = resolver.resolve(dir_path, "./existing_module").unwrap(); + + assert_eq!(ts_module_path.canonicalize().unwrap(), resolved.path()); + } + + #[test] + fn test_resolve_nonexistent_module() { + let dir = tempdir().unwrap(); + let dir_path = dir.path(); + + let resolver = create_resolver(&dir_path.to_path_buf()); + let resolved = resolver.resolve(dir_path, "./nonexistent_module"); + + assert!( + resolved.is_err(), + "Resolver should return an error for nonexistent_module" + ); + } + + #[test] + fn test_resolve_ts_reference() { + let dir = tempdir().unwrap(); + let dir_path = dir.path(); + + let tsconfig_path = dir_path.join("tsconfig.json"); + let tsconfig_json = serde_json::json!({ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/components": ["src/components/index.ts"] + } + } + }) + .to_string(); + + let mut tsconfig_file = File::create(&tsconfig_path).unwrap(); + tsconfig_file.write(tsconfig_json.as_bytes()).unwrap(); + + let module_dir = dir_path.join("src/components"); + let module_path = module_dir.join("index.ts"); + + fs::create_dir_all(module_dir.clone()).unwrap(); + File::create(&module_path).unwrap(); + + let resolver = create_resolver(&module_dir.to_path_buf()); + let resolved = resolver.resolve(module_dir, "@/components").unwrap(); + + assert_eq!(module_path.canonicalize().unwrap(), resolved.path()); + } +} diff --git a/crates/wyw_shaker/src/export.rs b/crates/wyw_shaker/src/export.rs new file mode 100644 index 0000000..35eb23b --- /dev/null +++ b/crates/wyw_shaker/src/export.rs @@ -0,0 +1,252 @@ +use crate::module_source::ModuleSource; +use oxc::allocator::Allocator; +use oxc::span::{Atom, Span}; +use oxc_resolver::Resolver; +use std::fmt::Debug; +use std::path::Path; + +#[derive(Clone, Debug, PartialEq)] +pub enum ExportedValue<'a> { + BigIntLiteral(Atom<'a>), + BooleanLiteral(bool), + Identifier(Atom<'a>), + NullLiteral, + NumericLiteral(f64), + Span(Span), + StringLiteral(Atom<'a>), + Void0, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum Export<'a> { + #[default] + Default, // TODO: handle value + + Named { + local: ExportedValue<'a>, + exported: Atom<'a>, + }, + + Reexport { + orig: Atom<'a>, + exported: Atom<'a>, + source: ModuleSource<'a>, + }, + + ReexportAll { + source: ModuleSource<'a>, + }, + + ReexportNamespace { + exported: Atom<'a>, + source: ModuleSource<'a>, + }, +} + +impl<'a> Export<'a> { + fn order(&self) -> usize { + match self { + Self::Default => 0, + Self::Named { .. } => 1, + Self::Reexport { .. } => 2, + Self::ReexportAll { .. } => 3, + Self::ReexportNamespace { .. } => 4, + } + } +} + +impl Ord for Export<'_> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (a, b) if a.order() != b.order() => a.order().cmp(&b.order()), + (Self::Named { exported: a, .. }, Self::Named { exported: b, .. }) => a.cmp(b), + ( + Self::Reexport { + exported: a_name, + source: a_source, + .. + }, + Self::Reexport { + exported: b_name, + source: b_source, + .. + }, + ) => { + if a_source != b_source { + a_source.cmp(b_source) + } else { + a_name.cmp(b_name) + } + } + (Self::ReexportAll { source: a }, Self::ReexportAll { source: b }) => a.cmp(b), + (_, _) => std::cmp::Ordering::Equal, + } + } +} + +impl<'a> PartialOrd for Export<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for Export<'_> {} + +#[derive(Clone)] +pub struct Exports<'a> { + allocator: &'a Allocator, + directory: &'a Path, + pub es_module: bool, + pub list: Vec>, + resolver: &'a Resolver, +} + +impl<'a> Debug for Exports<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.list.iter()).finish() + } +} + +impl<'a> Exports<'a> { + pub fn new(allocator: &'a Allocator, resolver: &'a Resolver, directory: &'a Path) -> Self { + Self { + allocator, + directory, + es_module: false, + list: Vec::new(), + resolver, + } + } + + pub fn add(&mut self, export: Export<'a>) { + if let Export::Named { exported, .. } = &export { + if exported == "__esModule" { + self.mark_as_es_module(); + + return; + } + } + + self.list.push(export); + } + + pub fn add_default(&mut self) { + self.add(Export::Default); + } + + pub fn add_named(&mut self, local: ExportedValue<'a>, exported: &Atom<'a>) { + self.add(Export::Named { + local, + exported: exported.clone(), + }); + } + + pub fn add_reexport(&mut self, orig: &Atom<'a>, exported: &Atom<'a>, source: &ModuleSource<'a>) { + self.add(Export::Reexport { + orig: orig.clone(), + exported: exported.clone(), + source: source.as_resolved(self.allocator, self.resolver, self.directory), + }); + } + + pub fn add_unresolved_reexport( + &mut self, + orig: &Atom<'a>, + exported: &Atom<'a>, + source: &Atom<'a>, + ) { + self.add_reexport(orig, exported, &ModuleSource::Unresolved(source.clone())); + } + + pub fn add_reexport_all(&mut self, source: &ModuleSource<'a>) { + self.add(Export::ReexportAll { + source: source.as_resolved(self.allocator, self.resolver, self.directory), + }); + } + + pub fn add_unresolved_reexport_all(&mut self, source: &Atom<'a>) { + self.add_reexport_all(&ModuleSource::Unresolved(source.clone())); + } + + pub fn add_reexport_namespace(&mut self, exported: &Atom<'a>, source: &ModuleSource<'a>) { + self.add(Export::ReexportNamespace { + exported: exported.clone(), + source: source.as_resolved(self.allocator, self.resolver, self.directory), + }); + } + + pub fn add_unresolved_reexport_namespace(&mut self, exported: &Atom<'a>, source: &Atom<'a>) { + self.add_reexport_namespace(exported, &ModuleSource::Unresolved(source.clone())); + } + + pub fn mark_as_es_module(&mut self) { + self.es_module = true; + } +} + +impl<'a> IntoIterator for Exports<'a> { + type Item = Export<'a>; + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.list.into_iter() + } +} + +#[derive(Clone, Debug, Default)] +pub struct ExportArea<'a> { + pub export: Export<'a>, + pub span: Span, +} + +#[cfg(test)] +mod tests { + use crate::default_resolver::create_resolver; + + use super::*; + use oxc::allocator::Allocator; + use oxc::span::Atom; + use std::fs::File; + use tempfile::tempdir; + + #[test] + fn test_resolved_reexports() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_path_buf(); + + let allocator = Allocator::default(); + let resolver = create_resolver(&dir_path); + + let mut exports = Exports::new(&allocator, &resolver, &dir_path); + + let utils_source = ModuleSource::Unresolved(Atom::from("./utils")); + let components_source = ModuleSource::Unresolved(Atom::from("./components")); + + let utils_path = dir_path.join("utils.ts"); + let components_path = dir_path.join("components.ts"); + + File::create(utils_path.clone()).unwrap(); + File::create(components_path.clone()).unwrap(); + + // "export { doSome } from './utils'" + exports.add_reexport(&Atom::from("doSome"), &Atom::from("doSome"), &utils_source); + // "export * from './components'" + exports.add_reexport_all(&components_source); + + assert_eq!(exports.list.len(), 2); + assert_eq!( + exports.list[0], + Export::Reexport { + orig: Atom::from("doSome"), + exported: Atom::from("doSome"), + source: ModuleSource::Resolved(&utils_path.canonicalize().unwrap().as_path()), + } + ); + assert_eq!( + exports.list[1], + Export::ReexportAll { + source: ModuleSource::Resolved(&components_path.canonicalize().unwrap().as_path()), + } + ); + } +} diff --git a/crates/wyw_shaker/src/ident_usages.rs b/crates/wyw_shaker/src/ident_usages.rs new file mode 100644 index 0000000..a62ba75 --- /dev/null +++ b/crates/wyw_shaker/src/ident_usages.rs @@ -0,0 +1,241 @@ +use crate::declaration_context::{DeclarationContext, PathPart}; +use oxc::ast::ast::Expression; +use oxc::span::{Atom, Span}; +use oxc_semantic::{IsGlobalReference, SymbolTable}; +use std::collections::HashMap; +use wyw_traverse::symbol::Symbol; +use wyw_traverse::{Ancestor, AnyNode}; + +#[derive(Debug)] +pub enum IdentUsage<'a> { + MemberExpression(Span, &'a Symbol, Atom<'a>), + ReexportAll(Span), + Uncertain(Span), + Unpacked { + span: Span, + local: &'a Symbol, + path: Atom<'a>, + symbol: Symbol, + }, +} + +impl<'a> IdentUsage<'a> { + pub fn prop(&self) -> Option<&Atom<'a>> { + match self { + Self::MemberExpression(_, _, prop) => Some(prop), + Self::Unpacked { path, .. } => Some(path), + _ => None, + } + } + + pub fn span(&self) -> &Span { + match self { + Self::MemberExpression(span, _, _) => span, + Self::ReexportAll(span) => span, + Self::Uncertain(span) => span, + Self::Unpacked { span, .. } => span, + } + } +} + +#[derive(Default)] +pub struct IdentUsages<'a> { + // symbols: &'a SymbolTable, + map: HashMap<&'a Symbol, Vec>>, +} + +impl<'a> IdentUsages<'a> { + fn add_usage(&mut self, usage: IdentUsage<'a>, symbol: &'a Symbol) { + let usages = self.map.get_mut(&symbol); + match usages { + None => { + self.map.insert(symbol, vec![usage]); + } + + Some(v) => { + v.push(usage); + } + } + } + + pub fn add_member_usage(&mut self, span: &Span, usage: Atom<'a>, symbol: &'a Symbol) { + let usage = IdentUsage::MemberExpression(*span, symbol, usage); + self.add_usage(usage, symbol); + } + + fn add_unpacked_usage(&mut self, span: &Span, to: &'a Symbol, path: Atom<'a>, from: Symbol) { + let usage = IdentUsage::Unpacked { + span: *span, + local: to, + path, + symbol: from, + }; + + self.add_usage(usage, to); + } + + pub fn get(&self, symbol: &'a Symbol) -> Option<&Vec>> { + self.map.get(symbol) + } + + fn mark_identifier_as_unresolvable(&mut self, span: &Span, symbol: &'a Symbol) { + let usage = IdentUsage::Uncertain(*span); + self.add_usage(usage, symbol); + } + + fn mark_identifier_as_reexport(&mut self, span: &Span, symbol: &'a Symbol) { + let usage = IdentUsage::ReexportAll(*span); + self.add_usage(usage, symbol); + } + + fn object_is_member( + &self, + expr: &'a Expression<'a>, + method_name: &str, + symbols: &'a SymbolTable, + ) -> bool { + if let Expression::StaticMemberExpression(ident) = &expr { + if ident.property.name != method_name { + return false; + } + + if let Expression::Identifier(id_ref) = &ident.object { + return id_ref.is_global_reference_name("Object", symbols); + } + } + + false + } + + pub fn resolve_identifier_usage( + &mut self, + parent: &Ancestor<'a>, + span: &Span, + symbol: &'a Symbol, + symbols: &'a SymbolTable, + ) { + // if ctx.ancestors.iter().any(|ancestor| ancestor.is_via_ts_type()) { + // return; + // } + + // let import = self.imports.find_by_symbol(symbol); + // + // if import.is_some() { + // let mut import = import.unwrap(); + // + // // If the source is unresolved, we have to resolve it first + // if let Import::Named { + // source: Source::Unresolved(source), + // .. + // } = import + // { + // let resolved = self.resolver.resolve(self.directory, source); + // if let Ok(resolution) = resolved { + // let resolution = self.allocator.alloc(resolution); + // let path = self.allocator.alloc(resolution.path()); + // import = import.set_resolved(path); + // + // if let Some(package_json) = resolution.package_json() { + // let raw_json = package_json.raw_json(); + // + // if let Some(obj) = raw_json.as_object() { + // if let Some(wyw) = obj.get("wyw-in-js") { + // if let Some(tags) = wyw.get("tags") { + // if let Some(tag) = tags.get(symbol.name) { + // // processor here is a relative path to the processor file + // let processor = tag.as_str().unwrap(); + // + // let full_path = package_json + // .path + // .parent() + // .map(|p| p.join(processor).normalize()) + // .unwrap(); + // + // import.set_processor(full_path); + // } + // } + // } + // } + // } + // } + // } + // + // if let Import::Named { + // processor: Processor::Resolved(processor), + // source: Source::Resolved(source), + // .. + // } = import + // { + // let idx = self.meta.processor_params.len(); + // let (span, processor_params) = ProcessorParams::from_ident( + // ctx, + // span, + // symbol, + // self.declaration_context.get_declaring_symbol(), + // idx, + // self.file_name, + // ); + // + // if !processor_params.is_empty() { + // self.meta.processor_params.push((span, processor_params)); + // } + // } + // } + + match parent { + Ancestor::Field(AnyNode::StaticMemberExpression(member_expr), "object") => { + self.add_member_usage(span, member_expr.property.name.clone(), symbol); + } + + Ancestor::Field(AnyNode::ComputedMemberExpression(member_expr), "object") => { + if let Expression::StringLiteral(literal) = &member_expr.expression { + self.add_member_usage(span, literal.value.clone(), symbol); + } else { + self.mark_identifier_as_unresolvable(span, symbol); + } + } + + Ancestor::Field(AnyNode::VariableDeclarator(declarator), "init") => { + let mut usages = vec![]; + let declaration_context = DeclarationContext::from(declarator); + + if let DeclarationContext::List(list) = declaration_context { + if list.is_empty() { + self.mark_identifier_as_unresolvable(span, symbol); + return; + } else { + for decl in list { + if decl.from.is_empty() { + self.mark_identifier_as_unresolvable(span, symbol); + return; + } + + match &decl.from[0] { + PathPart::Member(ident) => { + usages.push((decl.symbol, ident.clone())); + } + PathPart::Index(_) => {} + } + } + } + } + + for (from, usage) in usages { + self.add_unpacked_usage(span, symbol, usage.clone(), from); + } + } + + Ancestor::ListItem(AnyNode::CallExpression(call_expr), "arguments", _) => { + if self.object_is_member(&call_expr.callee, "keys", symbols) { + self.mark_identifier_as_reexport(span, symbol); + } else { + self.mark_identifier_as_unresolvable(span, symbol); + } + } + + _ => { + self.mark_identifier_as_unresolvable(span, symbol); + } + } + } +} diff --git a/crates/wyw_shaker/src/import.rs b/crates/wyw_shaker/src/import.rs new file mode 100644 index 0000000..3336772 --- /dev/null +++ b/crates/wyw_shaker/src/import.rs @@ -0,0 +1,351 @@ +use crate::module_source::ModuleSource; +use oxc::allocator::Allocator; +use oxc::span::Atom; +use oxc_resolver::Resolver; +use std::fmt::Debug; +use std::path::Path; +use wyw_traverse::local_identifier::LocalIdentifier; +use wyw_traverse::symbol::Symbol; + +#[derive(Clone, PartialEq)] +pub enum Import<'a> { + Default { + source: ModuleSource<'a>, + local: LocalIdentifier<'a>, + }, + + Named { + source: ModuleSource<'a>, + imported: Atom<'a>, + local: LocalIdentifier<'a>, + }, + + Namespace { + source: ModuleSource<'a>, + local: &'a Symbol, + }, + + SideEffect { + source: ModuleSource<'a>, + }, +} + +impl<'a> Debug for Import<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + _named @ Self::Named { + source, + imported, + local, + } => f + .debug_struct("Named") + .field("source", source) + .field("imported", imported) + .field("local", local) + .finish(), + Self::Default { source, local } => f + .debug_struct("Default") + .field("source", source) + .field("local", local) + .finish(), + Self::Namespace { source, local } => f + .debug_struct("Namespace") + .field("source", source) + .field("local", local) + .finish(), + Self::SideEffect { source } => f + .debug_struct("SideEffect") + .field("source", source) + .finish(), + } + } +} + +impl<'a> Import<'a> { + pub fn local(&self) -> Option> { + match self { + Self::Default { local, .. } => Some(local.clone()), + Self::Named { local, .. } => Some(local.clone()), + Self::Namespace { local, .. } => Some(LocalIdentifier::Identifier(<&Symbol>::clone(local))), + Self::SideEffect { .. } => None, + } + } + + // TODO Should be implemented differently + // pub fn processor(&self) -> Option<&PathBuf> { + // match self { + // Self::Named { + // source, imported, .. + // } => { + // if let ImportSource::Resolved(_, WywConfig::Resolved { tags }) = source { + // if let Some(tag) = tags.iter().find(|tag| tag.name == imported.as_str()) { + // return Some(&tag.processor); + // } + // } + + // None + // } + // _ => None, + // } + // } + + pub fn source(&self) -> &ModuleSource<'a> { + match self { + Self::Default { source, .. } + | Self::Named { source, .. } + | Self::Namespace { source, .. } + | Self::SideEffect { source } => source, + } + } + + fn order(&self) -> usize { + match self { + Self::Default { .. } => 0, + Self::Named { .. } => 1, + Self::Namespace { .. } => 2, + Self::SideEffect { .. } => 3, + } + } + + pub fn set_source(&mut self, source: ModuleSource<'a>) -> &mut Self { + *self = match self { + Self::Default { local, .. } => Self::Default { + source, + local: local.clone(), + }, + Self::Named { + imported, local, .. + } => Self::Named { + source, + imported: imported.clone(), + local: local.clone(), + }, + Self::Namespace { local, .. } => Self::Namespace { source, local }, + Self::SideEffect { .. } => Self::SideEffect { source }, + }; + + self + } + + // pub fn set_processor(&mut self, processor: PathBuf) { + // if let Self::Named { processor: p, .. } = self { + // *p = Processor::Resolved(processor); + // } else { + // todo!("set_processor for {:?}", self); + // } + // } +} + +impl Ord for Import<'_> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (a, b) if a.order() != b.order() => a.order().cmp(&b.order()), + (a, b) if a.source() != b.source() => a.source().cmp(b.source()), + (Self::Named { imported: a, .. }, Self::Named { imported: b, .. }) => a.cmp(b), + (_, _) => std::cmp::Ordering::Equal, + } + } +} + +impl<'a> PartialOrd for Import<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for Import<'_> {} + +#[derive(Clone)] +pub struct Imports<'a> { + allocator: &'a Allocator, + directory: &'a Path, + pub list: Vec>, + resolver: &'a Resolver, +} + +impl<'a> Debug for Imports<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.list.iter()).finish() + } +} + +impl<'a> Imports<'a> { + pub fn new(allocator: &'a Allocator, resolver: &'a Resolver, directory: &'a Path) -> Self { + Self { + allocator, + directory, + list: Vec::new(), + resolver, + } + } + + pub fn add(&mut self, import: Import<'a>) { + match import.source() { + ModuleSource::Resolved(_) => { + self.list.push(import); + } + unresolved @ ModuleSource::Unresolved(_) => { + let resolved = unresolved.as_resolved(self.allocator, self.resolver, self.directory); + let mut import = import.clone(); + import.set_source(resolved); + self.list.push(import); + } + } + } + + pub fn add_side_effect(&mut self, source: &ModuleSource<'a>) { + self.add(Import::SideEffect { + source: source.clone(), + }); + } + + pub fn add_side_effect_unresolved(&mut self, source: &Atom<'a>) { + self.add_side_effect(&ModuleSource::Unresolved(source.clone())); + } + + pub fn add_default(&mut self, source: &ModuleSource<'a>, local: &LocalIdentifier<'a>) { + self.add(Import::Default { + source: source.clone(), + local: local.clone(), + }); + } + + pub fn add_unresolved_default(&mut self, source: &Atom<'a>, local: &LocalIdentifier<'a>) { + self.add_default(&ModuleSource::Unresolved(source.clone()), local); + } + + pub fn add_named( + &mut self, + source: &ModuleSource<'a>, + imported: &Atom<'a>, + local: &LocalIdentifier<'a>, + ) { + // FIXME: it might be a legit named import. We have to check source file to be sure. + if imported == "default" { + self.add(Import::Default { + source: source.clone(), + local: local.clone(), + }); + + return; + } + + self.add(Import::Named { + source: source.clone(), + imported: imported.clone(), + local: local.clone(), + }); + } + + pub fn add_unresolved_named( + &mut self, + source: &Atom<'a>, + imported: &Atom<'a>, + local: &LocalIdentifier<'a>, + ) { + self.add_named(&ModuleSource::Unresolved(source.clone()), imported, local); + } + + pub fn add_namespace(&mut self, source: &ModuleSource<'a>, local: &'a Symbol) { + self.add(Import::Namespace { + source: source.clone(), + local, + }); + } + + pub fn add_unresolved_namespace(&mut self, source: &Atom<'a>, local: &'a Symbol) { + self.add_namespace(&ModuleSource::Unresolved(source.clone()), local); + } + + pub fn find_all_by_source(&self, expected: &str) -> Vec<&Import<'a>> { + self + .list + .iter() + .filter(|import| import.source() == expected) + .collect() + } + + pub fn find_ns_by_source(&self, expected: &str) -> Option<&'a Symbol> { + self.list.iter().find_map(|import| { + if let Import::Namespace { source, local } = import { + if source == expected { + return Some(*local); + } + } + + None + }) + } + + pub fn find_by_symbol(&mut self, symbol: &Symbol) -> Option<&mut Import<'a>> { + self.list.iter_mut().find(|import| { + if let Some(LocalIdentifier::Identifier(local)) = import.local() { + return local == symbol; + } + + false + }) + } +} + +impl<'a> IntoIterator for Imports<'a> { + type Item = Import<'a>; + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.list.into_iter() + } +} + +#[cfg(test)] +mod tests { + use crate::default_resolver::create_resolver; + + use super::*; + use oxc::allocator::Allocator; + use oxc::span::Span; + use oxc_index::Idx; + use oxc_semantic::SymbolId; + use std::fs::File; + use tempfile::tempdir; + + #[test] + fn test_import_local_accessor() { + let binding = Symbol::new_with_name( + "local".to_string(), + SymbolId::from_usize(0), + Span::new(0, 0), + ); + let ident = LocalIdentifier::Identifier(&binding); + + let default_import = Import::Default { + source: ModuleSource::Unresolved(Atom::from("source")), + local: ident.clone(), + }; + let side_effect = Import::SideEffect { + source: ModuleSource::Unresolved(Atom::from("source")), + }; + + assert_eq!(default_import.local(), Some(ident)); + assert_eq!(side_effect.local(), None); + } + + #[test] + fn test_import_source_resolution() { + let dir = tempdir().unwrap(); + let dir_path = dir.path(); + + let resolver = create_resolver(&dir_path.to_path_buf()); + let module_path = dir_path.join("foo.ts"); + File::create(&module_path).unwrap(); + + let allocator = Allocator::default(); + let import_source = ModuleSource::Unresolved(Atom::from("./foo")); + let resolved = import_source.as_resolved(&allocator, &resolver, dir_path); + + assert_eq!( + resolved, + ModuleSource::Resolved(module_path.canonicalize().unwrap().as_path()) + ); + } +} diff --git a/crates/wyw_shaker/src/lib.rs b/crates/wyw_shaker/src/lib.rs new file mode 100644 index 0000000..deb479f --- /dev/null +++ b/crates/wyw_shaker/src/lib.rs @@ -0,0 +1,1303 @@ +use crate::default_resolver::create_resolver; +use crate::meta::Meta; +use crate::references::References; +use crate::replacements::Replacements; +use itertools::Itertools; +use oxc::allocator::Allocator; +use oxc::ast::ast::*; +use oxc::parser::{ParseOptions, Parser}; +use oxc::span::GetSpan; +use oxc_semantic::{Semantic, SymbolTable}; +use std::path::Path; +use wyw_processor::replacement_value::ReplacementValue; +use wyw_traverse::{walk, Ancestor, AnyNode, EnterAction, TraverseCtx, TraverseHooks}; + +pub mod declaration_context; +pub mod default_resolver; +pub mod export; +pub mod ident_usages; +pub mod import; +pub mod meta; +mod module_source; +pub mod references; +pub mod replacements; + +#[derive(Default)] +pub struct ShakerOptions { + pub remove_jsx_and_hooks: bool, +} + +// fn get_callee<'a>(p: &'a CallExpression<'a>) -> &'a Expression<'a> { +// match &p.callee { +// Expression::SequenceExpression(sequence) if sequence.expressions.len() == 2 => { +// if let Expression::NumericLiteral(value) = &sequence.expressions[0] { +// if value.raw == "0" { +// return &sequence.expressions[1]; +// } +// } +// } +// _ => {} +// } +// +// &p.callee +// } + +struct Shaker<'a> { + changed: bool, + // meta: &'a Meta<'a>, + options: ShakerOptions, + references: References<'a>, + replacements: Replacements, +} + +impl<'a> Shaker<'a> { + // fn is_jsxruntime(&self, expr: &CallExpression, _ctx: &TraverseCtx<'a>) -> bool { + // let jsxruntime_source = "react/jsx-runtime"; + // + // let runtime_imports = self.meta.imports.find_all_by_source(jsxruntime_source); + // + // let callee = get_callee(expr); + // if let Expression::Identifier(ident) = callee { + // // FIXME: scope should be checked + // return runtime_imports + // .iter() + // .filter_map(|i| match i.local() { + // Some(LocalIdentifier::Identifier(local)) => Some(local), + // _ => None, + // }) + // .any(|local| local.name == ident.name.as_str()); + // } + // + // false + // } + // + // fn is_unnecessary_react_call(&self, expr: &CallExpression, ctx: &TraverseCtx<'a>) -> bool { + // self.is_jsxruntime(expr, ctx) + // } + + fn mark_jsx_class_component_as_unnecessary(&mut self, node: &Class) { + match &node.id { + Some(id) => { + // Named class component + let name = &id.name; + // FIXME: fix references + self.replace_with_text( + node, + format!("function {name}() {{ return null; }}").as_str(), + ); + } + None => { + // Anonymous class component + self.replace_with_text(node, "function() { return null; }"); + } + }; + } + + pub fn mark_react_component_as_unnecessary(&mut self, ctx: &TraverseCtx<'a>, call_span: Span) { + if !self.options.remove_jsx_and_hooks { + return; + } + + if self.is_span_for_change(call_span) { + // Already marked as unnecessary + return; + } + + let mut ancestors: Vec<&Ancestor> = ctx + .ancestors + .iter() + .rev() + .skip_while(|ancestor| { + !matches!( + ancestor, + Ancestor::Field(AnyNode::MethodDefinition(_), "value") + | Ancestor::Field(AnyNode::ArrowFunctionExpression(_), "body") + | Ancestor::Field(AnyNode::Function(_), "body") + ) + }) + .collect(); + + if ancestors.is_empty() { + self.replace_with_text(&call_span, "null"); + return; + } + + if matches!( + ancestors.get(1), + Some(Ancestor::Field(AnyNode::MethodDefinition(_), "value")) + ) { + ancestors = ancestors[1..].to_vec(); + } + + let fn_def = ancestors.first().unwrap(); + + match fn_def { + Ancestor::Field(AnyNode::ArrowFunctionExpression(expr), "body") => { + self.replace_with_text(&expr.span, "() => null"); + } + + Ancestor::Field(AnyNode::Function(expr), "body") => { + if let Some(body) = &expr.body { + self.replace_with_text(&body.span, "{ return null; }"); + } + } + + Ancestor::Field(AnyNode::MethodDefinition(method), "value") => { + if method.key.is_specific_id("render") { + let class = ctx + .ancestors + .iter() + .rfind(|ancestor| matches!(ancestor, Ancestor::Field(AnyNode::Class(_), "body"))) + .expect("Method definition without a class"); + + if let Ancestor::Field(AnyNode::Class(cls), "body") = class { + self.mark_jsx_class_component_as_unnecessary(cls); + } + } + } + _ => { + dbg!(fn_def); + } + } + } + + // pub fn necessity_check_call(&mut self, expr: &CallExpression<'a>, ctx: &TraverseCtx<'a>) { + // let span = expr.span; + // if self.is_span_for_change(span) { + // // Already marked as unnecessary + // return; + // } + // + // if self.is_unnecessary_react_call(&expr, ctx) { + // self.mark_react_component_as_unnecessary(ctx, span); + // } + // } +} + +impl<'a> TraverseHooks<'a> for Shaker<'a> { + fn should_skip(&self, node: &AnyNode) -> bool { + self.is_for_change(node) + } + + fn exit_program(&mut self, node: &'a Program<'a>, _ctx: &mut TraverseCtx<'a>) { + self.remove_delimiters(&node.body); + } + + fn exit_logical_expression(&mut self, node: &LogicalExpression<'a>, _ctx: &mut TraverseCtx<'a>) { + match &node.operator { + LogicalOperator::And => { + if self.is_for_delete(&node.left) || self.is_for_delete(&node.right) { + self.replace_with_undefined(node); + } + } + + LogicalOperator::Or | LogicalOperator::Coalesce => { + if self.is_for_delete(&node.left) && self.is_for_delete(&node.right) { + self.replace_with_undefined(node); + } else if self.is_for_delete(&node.left) { + self.replace_with_another(node, &node.right); + } else if self.is_for_delete(&node.right) { + self.replace_with_another(node, &node.left); + } + } + } + } + + fn exit_conditional_expression( + &mut self, + node: &'a ConditionalExpression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_for_delete(&node.alternate) + && (self.is_for_delete(&node.consequent) || self.is_for_delete(&node.test)) + { + self.replace_with_undefined(&node.span); + return; + } + + if self.is_for_delete(&node.consequent) { + self.replace_with_undefined(&node.consequent.span()); + } + + if self.is_for_delete(&node.alternate) { + self.replace_with_undefined(&node.alternate.span()); + } + + if self.is_for_delete(&node.test) { + self.replace_with_another(&node.span(), &node.alternate); + } + } + + fn exit_assignment_expression( + &mut self, + node: &'a AssignmentExpression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_for_delete(&node.right) { + todo!() + } + + if self.is_for_delete(&node.left) { + self.mark_for_delete(node.span); + } + } + + fn exit_array_assignment_target( + &mut self, + node: &ArrayAssignmentTarget<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_vec_opt_for_delete(&node.elements) { + self.mark_for_delete(node.span()); + } + } + + fn exit_object_assignment_target( + &mut self, + node: &'a ObjectAssignmentTarget<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_vec_for_delete(&node.properties) { + self.mark_for_delete(node.span()); + } else { + self.remove_delimiters(&node.properties); + } + } + + fn exit_assignment_target_property_identifier( + &mut self, + node: &'a AssignmentTargetPropertyIdentifier<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_for_delete(&node.binding) { + self.mark_for_delete(node.span) + } + } + + fn exit_assignment_target_property_property( + &mut self, + node: &'a AssignmentTargetPropertyProperty<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_for_delete(&node.name) || self.is_for_delete(&node.binding) { + self.mark_for_delete(node.span); + } + } + + fn exit_sequence_expression( + &mut self, + node: &'a SequenceExpression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if let Some(last_expr) = node.expressions.last() { + if self.is_for_delete(last_expr) { + self.replace_with_undefined(last_expr); + } + } + + self.remove_delimiters(&node.expressions); + } + + fn exit_parenthesized_expression( + &mut self, + node: &'a ParenthesizedExpression<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_for_delete(&node.expression) { + self.mark_for_delete(node.span); + } + } + + fn exit_variable_declaration( + &mut self, + node: &'a VariableDeclaration<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_vec_for_delete(&node.declarations) { + self.mark_for_delete(node.span()); + } + + self.remove_delimiters(&node.declarations); + } + + fn exit_variable_declarator( + &mut self, + node: &VariableDeclarator<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_for_delete(&node.id) { + self.mark_for_delete(node.span); + } + } + + fn exit_expression_statement( + &mut self, + node: &'a ExpressionStatement<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_for_delete(&node.expression) { + self.mark_for_delete(node.span); + } + } + + fn exit_debugger_statement(&mut self, node: &DebuggerStatement, _ctx: &mut TraverseCtx<'a>) { + self.mark_for_delete(node.span()); + } + + fn exit_assignment_pattern( + &mut self, + node: &'a AssignmentPattern<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if self.is_for_delete(&node.right) { + todo!() + } + + if self.is_for_delete(&node.left) { + self.mark_for_delete(node.span); + } + } + + fn exit_object_pattern(&mut self, node: &'a ObjectPattern<'a>, _ctx: &mut TraverseCtx<'a>) { + if self.is_vec_for_delete(&node.properties) { + self.mark_for_delete(node.span()); + } + + self.remove_delimiters(&node.properties); + } + + fn exit_binding_property(&mut self, node: &'a BindingProperty<'a>, _ctx: &mut TraverseCtx<'a>) { + if self.is_for_delete(&node.key) { + self.mark_for_delete(node.span); + } + + if self.is_for_delete(&node.value) { + self.mark_for_delete(node.span); + } + } + + fn exit_array_pattern(&mut self, node: &'a ArrayPattern<'a>, _ctx: &mut TraverseCtx<'a>) { + if self.is_vec_opt_for_delete(&node.elements) { + self.mark_for_delete(node.span()); + } + } + + fn exit_function(&mut self, node: &'a Function<'a>, _ctx: &mut TraverseCtx<'a>) { + if let Some(body) = &node.body { + if self.is_span_for_change(body.span) { + for item in node.params.items.iter() { + self.mark_for_delete(item.span); + } + + let last_param = node.params.items.last(); + self.remove_delimiters(&node.params.items); + + if let Some(rest) = &node.params.rest { + self.mark_for_delete(rest.span); + + if let Some(last_param) = last_param { + self.mark_for_delete(Span::new(last_param.span().end, rest.span().start)); + } + } + } + } + } + + fn exit_function_body(&mut self, node: &'a FunctionBody<'a>, _ctx: &mut TraverseCtx<'a>) { + self.remove_delimiters(&node.statements); + + if self.is_vec_for_delete(&node.statements) { + self.replace_with_text(node, "{}"); + } + } + + fn exit_class_body(&mut self, node: &'a ClassBody<'a>, _ctx: &mut TraverseCtx<'a>) { + self.remove_delimiters(&node.body); + + if self.is_vec_for_delete(&node.body) { + self.replace_with_text(node, "{}"); + } + } + + fn exit_method_definition(&mut self, node: &'a MethodDefinition<'a>, _ctx: &mut TraverseCtx<'a>) { + if self.is_for_delete(&node.key) { + self.mark_for_delete(node.span); + } + + if self.is_span_for_delete(node.value.span) { + self.mark_for_delete(node.span); + } + } + + fn exit_ts_interface_declaration( + &mut self, + node: &'a TSInterfaceDeclaration<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + self.mark_for_delete(node.span()); + } + + fn exit_export_named_declaration( + &mut self, + node: &'a ExportNamedDeclaration<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if !node.specifiers.is_empty() && self.is_vec_for_delete(&node.specifiers) { + self.mark_for_delete(node.span()); + } + + if let Some(declaration) = &node.declaration { + if self.is_for_delete(declaration) { + self.mark_for_delete(node.span()); + } + } + + self.remove_delimiters(&node.specifiers); + } + + fn exit_import_declaration( + &mut self, + node: &'a ImportDeclaration<'a>, + _ctx: &mut TraverseCtx<'a>, + ) { + if let Some(specifiers) = &node.specifiers { + if !specifiers.is_empty() && self.is_vec_for_delete(specifiers) { + self.mark_for_delete(node.span()); + } + + self.remove_delimiters(specifiers); + } + } + + fn enter_jsx_element(&mut self, node: &JSXElement<'a>, ctx: &mut TraverseCtx<'a>) -> EnterAction { + self.mark_react_component_as_unnecessary(ctx, node.span); + + EnterAction::Ignore + } + + fn enter_jsx_fragment( + &mut self, + node: &JSXFragment<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> EnterAction { + self.mark_react_component_as_unnecessary(ctx, node.span); + + EnterAction::Ignore + } +} + +impl<'a> Shaker<'a> { + pub fn new( + _meta: &'a Meta<'a>, + references: References<'a>, + replacements: Replacements, + options: ShakerOptions, + ) -> Self { + Self { + changed: false, + // meta, + references, + replacements, + options, + } + } + + fn is_span_for_change(&self, span: Span) -> bool { + self.replacements.has(span) + } + + fn is_for_change(&self, node: &impl GetSpan) -> bool { + self.is_span_for_change(node.span()) + } + + fn is_span_for_delete(&self, span: Span) -> bool { + self + .replacements + .get(span) + .is_some_and(|v| matches!(v.value, ReplacementValue::Del)) + } + + fn is_for_delete(&self, node: &impl GetSpan) -> bool { + self.is_span_for_delete(node.span()) + } + + fn is_vec_for_delete(&self, nodes: impl IntoIterator) -> bool { + nodes.into_iter().all(|node| self.is_for_delete(node)) + } + + fn is_vec_opt_for_delete( + &self, + nodes: impl IntoIterator>, + ) -> bool { + nodes + .into_iter() + .all(|el| !el.as_ref().is_some_and(|v| !self.is_for_delete(v))) + } + + fn mark_for_delete(&mut self, span: Span) { + self.changed |= self.replacements.add_deletion(span); + } + + fn remove_delimiters(&mut self, nodes: impl IntoIterator) { + let iter = nodes.into_iter(); + let tuples = iter.tuple_windows(); + let mut last_pair: Option<(&TItem, &TItem)> = None; + for (prev, next) in tuples { + last_pair = Some((prev, next)); + if self.is_for_delete(prev) { + self.mark_for_delete(Span::new(prev.span().end, next.span().start)); + } + } + + if let Some((penult, last)) = last_pair { + if self.is_for_delete(last) { + self.mark_for_delete(Span::new(penult.span().end, last.span().start)); + } + } + } + + fn replace_with_undefined(&mut self, node: &impl GetSpan) { + self.changed |= self + .replacements + .add_replacement(node.span(), ReplacementValue::Undefined); + } + + fn replace_with_text(&mut self, node: &impl GetSpan, text: &str) { + self.changed |= self + .replacements + .add_replacement(node.span(), ReplacementValue::Str(text.to_string())); + } + + fn replace_with_another(&mut self, node: &impl GetSpan, another: &impl GetSpan) { + self.changed |= self + .replacements + .add_replacement(node.span(), ReplacementValue::Span(another.span())); + } + + pub fn shake(&mut self, program: &'a Program<'a>, symbols: &'a SymbolTable) { + self.changed = false; + walk(self, program, symbols); + + // Remove all references to deleted nodes + let mut cloned_references = self.references.clone(); + cloned_references.apply_replacements(&self.replacements); + + let mut dead_symbols = vec![]; + for (&symbol, references) in &cloned_references.map { + if references.is_empty() { + dead_symbols.push(symbol); + } + } + + for dead_symbol in dead_symbols { + self.mark_for_delete(dead_symbol.decl); + } + + if self.changed { + self.shake(program, symbols); + } + } +} + +pub fn shake( + program: &Program, + meta: &Meta, + replacements: Replacements, + semantic: &Semantic, + allocator: &Allocator, + options: ShakerOptions, +) -> String { + let references = References::from_semantic(semantic, allocator); + let mut shaker = Shaker::new(meta, references, replacements, options); + + shaker.shake(program, semantic.symbols()); + + shaker.replacements.apply(semantic.source_text()) +} + +pub fn shake_source( + source_text: String, + replacements: Replacements, + options: ShakerOptions, +) -> String { + let allocator = Allocator::default(); + + let path = Path::new("test.js"); + let source_type = SourceType::from_path(path).unwrap(); + + let parser_ret = Parser::new(&allocator, &source_text, source_type) + .with_options(ParseOptions { + parse_regular_expression: true, + ..ParseOptions::default() + }) + .parse(); + + assert!(parser_ret.errors.is_empty()); + + let program = allocator.alloc(parser_ret.program); + + let semantic_ret = oxc_semantic::SemanticBuilder::new() + .build_module_record(path, program) + .with_check_syntax_error(true) + .build(program); + + let resolver = create_resolver(&path.to_path_buf()); + + let meta = Meta::new(&allocator, path, &resolver); + let res = shake( + program, + &meta, + replacements, + &semantic_ret.semantic, + &allocator, + options, + ); + if res == "\n" { + "".to_string() + } else { + res + } +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + use itertools::Itertools; + use regex::Regex; + + fn extract_spans_for_deletion(source_text: &str) -> (String, Vec) { + // Split the source text into lines + // For each line, check if it contains only ^ and spaces + // If it does, extract the span and add it to the list of spans for deletion + // If it doesn't, add the line to the new source text + let mut lines = vec![]; + let mut spans_for_deletion = Vec::new(); + let mut pos = 0; + let mut last_line_len = 0; + + let marker_line_re = Regex::new(r"[\s^]+$").unwrap(); + let marker_re = Regex::new(r"\^+").unwrap(); + + for line in source_text.split('\n') { + if marker_line_re.is_match(line) { + for marker in marker_re.find_iter(line) { + let start = pos - last_line_len + marker.start(); + let end = pos - last_line_len + marker.end(); + spans_for_deletion.push(Span::new(start as u32, end as u32)); + } + } else { + lines.push(line); + last_line_len = line.len() + 1; + pos += last_line_len; + } + } + + (lines.iter().join("\n"), spans_for_deletion) + } + + fn run(source_text: &str) -> String { + let (source_text, for_delete) = extract_spans_for_deletion(source_text); + shake_source( + source_text, + Replacements::from_spans(for_delete), + ShakerOptions { + remove_jsx_and_hooks: true, + }, + ) + } + + fn keep_jsx(source_text: &str) -> String { + let (source_text, for_delete) = extract_spans_for_deletion(source_text); + shake_source( + source_text, + Replacements::from_spans(for_delete), + ShakerOptions { + remove_jsx_and_hooks: false, + }, + ) + } + + #[test] + fn test_named_exports() { + assert_eq!( + run(indoc! {r#" + export { to_remove, to_keep }; + ^^^^^^^^^ + "#}), + indoc! {r#" + export { to_keep }; + "#} + ); + + assert_eq!( + run(indoc! {r#" + export { a, b, c }; + ^ + "#}), + indoc! {r#" + export { a, c }; + "#} + ); + + assert_eq!( + run(indoc! {r#" + export { a, b, c }; + ^ ^ + "#}), + indoc! {r#" + export { b }; + "#} + ); + + assert_eq!( + run(indoc! {r#" + export { to_remove }; + ^^^^^^^^^ + "#}), + indoc! {r#""#} + ); + + assert_eq!( + run(indoc! {r#" + export { to_remove_1, to_remove_2 }; + ^^^^^^^^^^^ ^^^^^^^^^^^ + "#}), + indoc! {r#""#} + ); + } + + #[test] + fn test_imports() { + assert_eq!( + run(indoc! {r#" + import { to_remove, to_keep } from "module"; + ^^^^^^^^^ + "#}), + indoc! {r#" + import { to_keep } from "module"; + "#} + ); + + assert_eq!( + run(indoc! {r#" + import { a, b, c } from "module"; + ^ + "#}), + indoc! {r#" + import { a, c } from "module"; + "#} + ); + + assert_eq!( + run(indoc! {r#" + import { a, b, c } from "module"; + ^ ^ + "#}), + indoc! {r#" + import { b } from "module"; + "#} + ); + + assert_eq!( + run(indoc! {r#" + import { to_remove } from "module"; + ^^^^^^^^^ + "#}), + indoc! {r#""#} + ); + + assert_eq!( + run(indoc! {r#" + import { to_remove_1, to_remove_2 } from "module"; + ^^^^^^^^^^^ ^^^^^^^^^^^ + "#}), + indoc! {r#""#} + ); + } + + #[test] + fn test_variable_declaration() { + assert_eq!( + run(indoc! {r#" + const a = 42; + ^ + "#}), + indoc! {r#""#} + ); + + assert_eq!( + run(indoc! {r#" + const a = 42, b = 24; + ^ + "#}), + indoc! {r#" + const b = 24; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = 42, b = 24; + ^ + "#}), + indoc! {r#" + const a = 42; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = 42, b = 24; + ^ ^ + "#}), + indoc! {r#""#} + ); + + assert_eq!( + run(indoc! {r#" + const { a: b } = { a: 42 } + ^ + "#}), + indoc! {r#""#} + ); + + assert_eq!( + run(indoc! {r#" + const { a = 1, b } = { + ^ + a: 42, + b: 24 + }; + "#}), + indoc! {r#" + const { b } = { + a: 42, + b: 24 + }; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const [a, b] = [42, 24]; + ^ + "#}), + indoc! {r#" + const [, b] = [42, 24]; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const [a, b] = [42, 24]; + ^ ^ + "#}), + indoc! {r#""#} + ); + } + + #[test] + fn test_assigment() { + assert_eq!( + run(indoc! {r#" + a = 42; + ^ + "#}), + indoc! {r#""#} + ); + + assert_eq!( + run(indoc! {r#" + a = 42, b = 24; + ^ + "#}), + indoc! {r#" + b = 24; + "#} + ); + + assert_eq!( + run(indoc! {r#" + a = 42, b = 24; + ^ + "#}), + indoc! {r#" + a = 42, undefined; + "#} + ); + + assert_eq!( + run(indoc! {r#" + a = 42, b = 24; + ^ ^ + "#}), + indoc! {r#" + undefined; + "#} + ); + + assert_eq!( + run(indoc! {r#" + ({ a: b } = { a: 42 }) + ^ + "#}), + indoc! {r#""#} + ); + + assert_eq!( + run(indoc! {r#" + ({ a = 1, b } = { + ^ + a: 42, + b: 24 + }); + "#}), + indoc! {r#" + ({ b } = { + a: 42, + b: 24 + }); + "#} + ); + + assert_eq!( + run(indoc! {r#" + [a, b] = [42, 24]; + ^ + "#}), + indoc! {r#" + [, b] = [42, 24]; + "#} + ); + + assert_eq!( + run(indoc! {r#" + [a, b] = [42, 24]; + ^ ^ + "#}), + indoc! {r#""#} + ); + } + + #[test] + fn test_sequence() { + assert_eq!( + run(indoc! {r#" + const a = (1, 2, 3, b); + ^ + "#}), + indoc! {r#" + const a = (1, 2, 3, undefined); + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = (1, 2, b, 3); + ^ + "#}), + indoc! {r#" + const a = (1, 2, 3); + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = (b, c, d); + ^ ^ ^ + "#}), + indoc! {r#" + const a = (undefined); + "#} + ); + } + + #[test] + fn test_conditional_expression() { + assert_eq!( + run(indoc! {r#" + const a = to_remove ? 42 : 24; + ^^^^^^^^^ + "#}), + indoc! {r#" + const a = 24; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = to_remove ? 42 : 24; + ^^ + "#}), + indoc! {r#" + const a = to_remove ? undefined : 24; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = to_remove ? 42 : 24; + ^^ + "#}), + indoc! {r#" + const a = to_remove ? 42 : undefined; + "#} + ); + } + + #[test] + fn test_logical_expression() { + assert_eq!( + run(indoc! {r#" + const a1 = b && c; + ^ + const a2 = b && c; + ^ + const a3 = b && c; + ^ ^ + "#}), + indoc! {r#" + const a1 = undefined; + const a2 = undefined; + const a3 = undefined; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a1 = b || c; + ^ + const a2 = b || c; + ^ + const a3 = b || c; + ^ ^ + "#}), + indoc! {r#" + const a1 = c; + const a2 = b; + const a3 = undefined; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a1 = b ?? c; + ^ + const a2 = b ?? c; + ^ + const a3 = b ?? c; + ^ ^ + "#}), + indoc! {r#" + const a1 = c; + const a2 = b; + const a3 = undefined; + "#} + ); + } + + #[test] + fn test_class() { + assert_eq!( + run(indoc! {r#" + const a = class { get method() { return; } }; + ^^^^^^ + "#}), + indoc! {r#" + const a = class {}; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = class { + get method_1() { + ^^^^^^^^ + return; + } + get method_2() { + return; + } + }; + "#}), + indoc! {r#" + const a = class { + get method_2() { + return; + } + }; + "#} + ); + } + + #[test] + fn test_function() { + // assert_eq!( + // run(indoc! {r#" + // const a = function to_remove(param) { + // ^^^^^ + // return; + // }; + // "#}), + // indoc! {r#" + // const a = function to_remove(param) { + // return; + // }; + // "#} + // ); + + assert_eq!( + run(indoc! {r#" + const a = function to_remove(param) { + return; + ^^^^^^^ + }; + "#}), + indoc! {r#" + const a = function to_remove() {}; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = function to_remove(param, ...rest) { + return; + ^^^^^^^ + }; + "#}), + indoc! {r#" + const a = function to_remove() {}; + "#} + ); + } + + #[test] + fn test_unused_declaration() { + assert_eq!( + run(indoc! {r#" + const a = 42; + const b = 24; + export { a, b }; + ^ + "#}), + indoc! {r#" + const a = 42; + export { a }; + "#} + ); + } + + #[test] + fn test_moved_reference() { + assert_eq!( + run(indoc! {r#" + const a = 42; + const b = 24; + const c = localStorage.isDebug ? a : b; + ^^^^^^^^^^^^^^^^^^^^ + export { c }; + "#}), + indoc! {r#" + const b = 24; + const c = b; + export { c }; + "#} + ); + } + + #[test] + fn test_remove_jsx() { + assert_eq!( + run(indoc! {r#" + const str = "to remove"; + const a =
{str}
; + "#}), + indoc! {r#" + const a = null; + "#} + ); + + assert_eq!( + run(indoc! {r#" + const a = <>to remove; + "#}), + indoc! {r#" + const a = null; + "#} + ); + } + + #[test] + fn test_keep_jsx() { + assert_eq!( + keep_jsx(indoc! {r#" + const str = "to remove"; + const a =
{str}
; + "#}), + indoc! {r#" + const str = "to remove"; + const a =
{str}
; + "#} + ); + + assert_eq!( + keep_jsx(indoc! {r#" + const a = <>to remove; + "#}), + indoc! {r#" + const a = <>to remove; + "#} + ); + } + + #[test] + fn test_replace_fn_component() { + assert_eq!( + run(indoc! {r#" + const Title1 = function(props) { + return

{props.children}

; + }; + + const Title2 = (props) =>

{props.children}

; + + function Title3(props) { + return

{props.children}

; + } + + export { Title1, Title2, Title3 }; + "#}), + indoc! {r#" + const Title1 = function() { return null; }; + + const Title2 = () => null; + + function Title3() { return null; } + + export { Title1, Title2, Title3 }; + "#} + ); + } + + #[test] + fn test_replace_class_component() { + assert_eq!( + run(indoc! {r#" + class Title { + someMethod() {} + + render() { + return

to remove

; + } + } + + export { Title }; + "#}), + indoc! {r#" + function Title() { return null; } + + export { Title }; + "#} + ); + } +} diff --git a/crates/wyw_shaker/src/meta.rs b/crates/wyw_shaker/src/meta.rs new file mode 100644 index 0000000..2f7e606 --- /dev/null +++ b/crates/wyw_shaker/src/meta.rs @@ -0,0 +1,119 @@ +use crate::export::{Export, Exports}; +use crate::import::{Import, Imports}; +use oxc::allocator::Allocator; +use oxc_resolver::Resolver; +use std::fmt::Debug; +use std::path::Path; +use wyw_processor::params::ProcessorCalls; + +pub struct Meta<'a> { + pub file_name: &'a Path, + + pub cjs: bool, + pub directory: &'a Path, + pub imports: Imports<'a>, + pub exports: Exports<'a>, + + pub processor_calls: ProcessorCalls<'a>, +} + +impl<'a> Debug for Meta<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut base = f.debug_struct("Meta"); + + let mut common_fields = base + .field("cjs", &self.cjs) + .field("es_module", &self.exports.es_module) + .field("imports", &self.imports) + .field("exports", &self.exports); + + if !self.processor_calls.is_empty() { + common_fields = common_fields.field("processor_calls", &self.processor_calls); + } + + common_fields.finish() + } +} + +impl<'a> Meta<'a> { + pub fn apply_patch(&mut self, patch: JsFilePatch<'a>) { + self + .imports + .list + .retain(|import| !patch.imports_for_delete.contains(import)); + self + .exports + .list + .retain(|export| !patch.exports_for_delete.contains(export)); + + self.imports.list.extend(patch.imports.list); + self.exports.list.extend(patch.exports.list); + + self.imports.list.sort(); + self.exports.list.sort(); + } + + pub fn new(allocator: &'a Allocator, file_name: &'a Path, resolver: &'a Resolver) -> Meta<'a> { + let directory = file_name.parent().unwrap(); + + Meta { + cjs: true, + directory, + file_name, + imports: Imports::new(allocator, resolver, directory), + exports: Exports::new(allocator, resolver, directory), + + processor_calls: Default::default(), + } + } +} + +pub struct JsFilePatch<'a> { + pub imports: Imports<'a>, + pub exports: Exports<'a>, + + pub imports_for_delete: Vec>, + pub exports_for_delete: Vec>, +} + +impl<'a> JsFilePatch<'a> { + pub fn new(allocator: &'a Allocator, resolver: &'a Resolver, directory: &'a Path) -> Self { + Self { + imports: Imports::new(allocator, resolver, directory), + exports: Exports::new(allocator, resolver, directory), + imports_for_delete: Vec::new(), + exports_for_delete: Vec::new(), + } + } + + pub fn delete_import(&mut self, import: &Import<'a>) { + self.imports_for_delete.push(import.clone()); + } + + pub fn delete_export(&mut self, export: &Export<'a>) { + self.exports_for_delete.push(export.clone()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::default_resolver::create_resolver; + use oxc::allocator::Allocator; + + use std::path::Path; + + #[test] + fn test_meta_new() { + let allocator = Allocator::default(); + let file_name = Path::new("test_file.js"); + let resolver = create_resolver(&file_name.to_path_buf()); + + let meta = Meta::new(&allocator, &file_name, &resolver); + + assert_eq!(meta.file_name, file_name); + + assert!(meta.imports.list.is_empty()); + assert!(meta.exports.list.is_empty()); + } +} diff --git a/crates/wyw_shaker/src/module_source.rs b/crates/wyw_shaker/src/module_source.rs new file mode 100644 index 0000000..06368ae --- /dev/null +++ b/crates/wyw_shaker/src/module_source.rs @@ -0,0 +1,92 @@ +use oxc::allocator::Allocator; +use oxc::span::Atom; +use oxc_resolver::Resolver; +use std::fmt::Debug; +use std::path::Path; + +#[derive(Clone, Eq, PartialEq)] +pub enum ModuleSource<'a> { + Unresolved(Atom<'a>), + Resolved(&'a Path), +} + +impl<'a> Debug for ModuleSource<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModuleSource::Unresolved(atom) => write!(f, "{:?}", atom), + ModuleSource::Resolved(path) => write!(f, "Resolved({:?})", path), + } + } +} + +impl<'a> PartialOrd for ModuleSource<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (ModuleSource::Unresolved(a), ModuleSource::Unresolved(b)) => Some(a.cmp(b)), + (ModuleSource::Resolved(a), ModuleSource::Resolved(b)) => Some(a.cmp(b)), + (ModuleSource::Unresolved(_), ModuleSource::Resolved(_)) => Some(std::cmp::Ordering::Less), + (ModuleSource::Resolved(_), ModuleSource::Unresolved(_)) => Some(std::cmp::Ordering::Greater), + } + } +} + +impl<'a> Ord for ModuleSource<'a> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap() + } +} + +impl<'a> PartialEq for ModuleSource<'a> { + fn eq(&self, other: &str) -> bool { + if let ModuleSource::Unresolved(path) = self { + return path == other; + } + + false + } +} + +impl<'a> ModuleSource<'a> { + pub fn as_unresolved(&self) -> Option<&Atom<'a>> { + match self { + ModuleSource::Unresolved(atom) => Some(atom), + _ => None, + } + } + + pub fn as_resolved( + &self, + allocator: &'a Allocator, + resolver: &Resolver, + directory: &Path, + ) -> Self { + match self { + ModuleSource::Unresolved(atom) => { + if let Ok(resolution) = resolver.resolve(directory, atom) { + let resolution = allocator.alloc(resolution); + + ModuleSource::Resolved(resolution.path()) + } else { + // TODO: this should be handled differently + panic!("failed to resolve {:?}", atom); + } + } + + ModuleSource::Resolved(_) => self.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_import_source_equality() { + let a = ModuleSource::Unresolved(Atom::from("test")); + let b = ModuleSource::Unresolved(Atom::from("other-module")); + + assert_eq!(a, a); + assert_ne!(a, b); + } +} diff --git a/crates/wyw_shaker/src/references.rs b/crates/wyw_shaker/src/references.rs new file mode 100644 index 0000000..0928896 --- /dev/null +++ b/crates/wyw_shaker/src/references.rs @@ -0,0 +1,282 @@ +use crate::replacements::Replacements; +use oxc::allocator::Allocator; +use oxc::ast::ast::{Expression, IdentifierReference, MemberExpression}; +use oxc::ast::AstKind; +use oxc::span::{GetSpan, Span}; +use oxc::syntax::reference::ReferenceFlags; +use oxc_semantic::{AstNode, AstNodes, NodeId, Semantic}; +use std::collections::HashMap; +use wyw_processor::replacement_value::ReplacementValue; +use wyw_traverse::symbol::Symbol; + +#[derive(Clone, Debug)] +pub struct Reference { + pub flags: ReferenceFlags, + pub span: Span, +} + +#[derive(Clone, Debug, Default)] +pub struct References<'a> { + pub map: HashMap<&'a Symbol, Vec>, +} + +fn get_parent_node<'a>(nodes: &'a AstNodes, node_id: NodeId) -> &'a AstNode<'a> { + let parent = nodes.parent_node(node_id); + parent.expect("Parent node should exist") +} + +fn is_mut_call(node: &AstNode) -> bool { + // Such as `Object.assign(obj, { key: value })` + if let AstKind::CallExpression(call_exp) = node.kind() { + if let Expression::StaticMemberExpression(member_exp) = &call_exp.callee { + if let Expression::Identifier(ident) = &member_exp.object { + return ident.name == "Object" && member_exp.property.name == "assign"; + } + } + } + + false +} + +fn is_write_ref(nodes: &AstNodes, node: &IdentifierReference, node_id: NodeId) -> bool { + let parent = get_parent_node(nodes, node_id); + + // Such as `obj.key = value` + if let AstKind::MemberExpression(MemberExpression::StaticMemberExpression(member_exp)) = + parent.kind() + { + if let Expression::Identifier(ident) = &member_exp.object { + if ident.name != node.name { + return false; + } + + let parent = get_parent_node(nodes, parent.id()); + return match parent.kind() { + AstKind::SimpleAssignmentTarget(_) => return true, + _ => false, + }; + } + } + + if let AstKind::Argument(_arg) = parent.kind() { + let parent = get_parent_node(nodes, parent.id()); + return is_mut_call(parent); + } + + false +} + +impl<'a> References<'a> { + pub fn add(&mut self, symbol: &'a Symbol, reference: Reference) { + if let std::collections::hash_map::Entry::Vacant(e) = self.map.entry(symbol) { + e.insert(vec![reference]); + } else { + self.map.get_mut(&symbol).unwrap().push(reference); + } + } + + pub fn get(&self, symbol: &'a Symbol) -> Option<&Vec> { + self.map.get(symbol) + } + + pub fn apply_replacements(&mut self, replacements: &Replacements) { + let mut moved_spans = vec![]; + for replacement in &replacements.list { + match replacement.value { + ReplacementValue::Del => {} + ReplacementValue::Span(from) => { + moved_spans.push((from, replacement.span)); + } + ReplacementValue::Str(_) => {} + ReplacementValue::Undefined => {} + } + } + + let mut new_refs = vec![]; + for (&symbol, refs) in self.map.iter_mut() { + for reference in &*refs { + for (from, to) in &moved_spans { + if reference.span.start >= from.start && reference.span.end <= from.end { + let delta: i32 = from.start as i32 - reference.span.start as i32; + new_refs.push(( + symbol, + Span::new( + (to.start as i32 - delta) as u32, + (to.end as i32 - delta) as u32, + ), + reference.flags, + )); + } + } + } + + refs.retain(|reference| !replacements.has(reference.span)); + } + + for (symbol, span, flags) in new_refs { + self + .map + .get_mut(symbol) + .unwrap() + .push(Reference { flags, span }); + } + } + + pub fn from_semantic(semantic: &Semantic, allocator: &'a Allocator) -> Self { + let symbols = semantic.symbols(); + let nodes = semantic.nodes(); + + let mut references = Self::default(); + + for reference in &symbols.references { + let symbol_id = reference.symbol_id(); + let decl_node = symbol_id + .and_then(|id| symbols.declarations.get(id)) + .map(|&decl| nodes.get_node(decl)); + if decl_node.is_none() { + // It's a reference to a built-in or unknown symbol + continue; + } + + let decl_node = decl_node.unwrap(); + let symbol_id = symbol_id.unwrap(); + let symbol = allocator.alloc(Symbol::new(symbols, symbol_id, decl_node.span())); + let node_id = reference.node_id(); + let node = nodes.get_node(node_id); + let node_kind = node.kind(); + + let flags = if let AstKind::IdentifierReference(ref_node) = node_kind { + if is_write_ref(nodes, ref_node, node_id) { + reference.flags().union(ReferenceFlags::Write) + } else { + reference.flags() + } + } else { + reference.flags() + }; + + references.add( + symbol, + Reference { + flags, + span: node_kind.span(), + }, + ); + } + + references + } +} + +#[cfg(test)] +mod test { + use super::*; + use indoc::indoc; + use oxc::allocator::Allocator; + use oxc::parser::{ParseOptions, Parser}; + use oxc::span::SourceType; + use oxc_semantic::Semantic; + use std::path::Path; + + fn parse<'a>(source: &'a str, allocator: &'a Allocator) -> Semantic<'a> { + let path = Path::new("test.ts"); + let source_type = SourceType::from_path(path).unwrap(); + + let parser_ret = Parser::new(allocator, source, source_type) + .with_options(ParseOptions { + parse_regular_expression: true, + ..ParseOptions::default() + }) + .parse(); + + let semantic_ret = oxc_semantic::SemanticBuilder::new() + .build_module_record(path, &parser_ret.program) + .with_check_syntax_error(true) + .build(&parser_ret.program); + + semantic_ret.semantic + } + + fn annotate(source: &str) -> String { + let allocator = Allocator::default(); + let semantic = parse(source, &allocator); + let references = References::from_semantic(&semantic, &allocator); + + let mut flat_refs = references + .map + .iter() + .flat_map(|(_symbol, refs)| refs) + .collect::>(); + + flat_refs.sort_by(|a, b| a.span.start.cmp(&b.span.start)); + + let mut chunks = vec![]; + let mut last_pos: usize = 0; + for reference in flat_refs { + let start = reference.span.start as usize; + let end = reference.span.end as usize; + if last_pos != start { + chunks.push(source[last_pos..end].to_string()); + } + + let mut flags = vec![]; + if reference.flags.contains(ReferenceFlags::Read) { + flags.push("Read"); + } + + if reference.flags.contains(ReferenceFlags::Write) { + flags.push("Write"); + } + + chunks.push(format!("/* {} */", flags.join(" | "))); + + last_pos = end; + } + + chunks.push(source[last_pos..].to_string()); + + chunks.join("") + } + + #[test] + fn test_object_assign() { + assert_eq!( + annotate(indoc! {r#" + const classes = {}; + Object.assign( + classes, + { + disabled: "disabled", + } + ); + export { classes }; + "#}), + indoc! {r#" + const classes = {}; + Object.assign( + classes/* Read | Write */, + { + disabled: "disabled", + } + ); + export { classes/* Read */ }; + "#} + ) + } + + #[test] + fn test_assign_to_prop() { + assert_eq!( + annotate(indoc! {r#" + const obj = {}; + obj.key = "value"; + export const key = obj.key; + "#}), + indoc! {r#" + const obj = {}; + obj/* Read | Write */.key = "value"; + export const key = obj/* Read */.key; + "#} + ) + } +} diff --git a/crates/wyw_shaker/src/replacements.rs b/crates/wyw_shaker/src/replacements.rs new file mode 100644 index 0000000..fa9961a --- /dev/null +++ b/crates/wyw_shaker/src/replacements.rs @@ -0,0 +1,238 @@ +use oxc::span::Span; +use std::cmp::Ordering; +use std::fmt::Debug; +use wyw_processor::replacement_value::ReplacementValue; + +#[derive(Debug, PartialEq)] +pub struct Replacement { + pub span: Span, + pub value: ReplacementValue, +} + +#[derive(Default)] +pub struct Replacements { + pub(crate) list: Vec, +} + +impl Debug for Replacements { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.list.iter()).finish() + } +} + +impl Replacements { + pub fn new(init: impl IntoIterator) -> Self { + let mut res = Self::default(); + for span in init { + res.add(span); + } + + res + } + + pub fn from_spans(init: impl IntoIterator) -> Self { + Self::new(init.into_iter().map(|span| Replacement { + span, + value: ReplacementValue::Del, + })) + } + + pub fn add(&mut self, new: Replacement) -> bool { + // If new span covers existing spans, replace them with the new span + let mut i = 0; + while i < self.list.len() { + let existing = self.list.get(i).unwrap(); + // FIXME: Simplify existing == new + if existing.span.start == new.span.start && existing.span.end == new.span.end { + if existing.value == new.value { + return false; + } + + self.list.remove(i); + } else if existing.span.start <= new.span.start && existing.span.end >= new.span.end { + return false; + } else if existing.span.start >= new.span.start && existing.span.end <= new.span.end { + self.list.remove(i); + } else if existing.span.end > new.span.start || new.span.end < existing.span.start { + break; + } else { + i += 1; + } + } + + // Insert the new span + self.list.insert(i, new); + true + } + + pub fn apply(&self, text: &str) -> String { + let mut chunks = vec![]; + let mut last_pos: usize = 0; + for replacement in &self.list { + let start = replacement.span.start as usize; + let end = replacement.span.end as usize; + if last_pos != start { + chunks.push(text[last_pos..start].to_string()); + } + + match &replacement.value { + ReplacementValue::Del => {} + ReplacementValue::Span(span) => { + chunks.push(text[span.start as usize..span.end as usize].to_string()); + } + ReplacementValue::Str(s) => { + chunks.push(s.to_string()); + } + ReplacementValue::Undefined => { + chunks.push("undefined".to_string()); + } + } + + last_pos = end; + } + + chunks.push(text[last_pos..].to_string()); + + chunks.join("") + } + + pub fn add_deletion(&mut self, span: Span) -> bool { + self.add_replacement(span, ReplacementValue::Del) + } + + pub fn add_replacement(&mut self, span: Span, value: ReplacementValue) -> bool { + self.add(Replacement { span, value }) + } + + pub fn get(&self, span: Span) -> Option<&Replacement> { + self + .list + .binary_search_by(|s| { + if span.start < s.span.start { + return Ordering::Greater; + } + + if span.end > s.span.end { + return Ordering::Less; + } + + Ordering::Equal + }) + .ok() + .and_then(|i| self.list.get(i)) + } + + pub fn has(&self, span: Span) -> bool { + self.get(span).is_some() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn del(start: u32, end: u32) -> Replacement { + Replacement { + span: Span::new(start, end), + value: ReplacementValue::Del, + } + } + + fn undefined(start: u32, end: u32) -> Replacement { + Replacement { + span: Span::new(start, end), + value: ReplacementValue::Undefined, + } + } + + #[test] + fn test_new() { + assert_eq!(Replacements::default().list, vec![]); + + assert_eq!(Replacements::new(vec![]).list, vec![]); + + assert_eq!( + Replacements::new(vec![del(1, 10), del(20, 30)]).list, + vec![del(1, 10), del(20, 30)] + ); + + assert_eq!( + Replacements::new(vec![del(1, 10), del(20, 30), del(1, 30)]).list, + vec![del(1, 30)] + ); + + assert_eq!( + Replacements::new(vec![del(20, 30), del(1, 10)]).list, + vec![del(1, 10), del(20, 30)] + ); + + assert_eq!( + Replacements::new(vec![del(9, 10), del(15, 16), del(10, 12), del(13, 15)]).list, + vec![del(9, 10), del(10, 12), del(13, 15), del(15, 16)] + ); + + assert_eq!( + Replacements::new(vec![del(9, 10), del(15, 16), del(0, 2)]).list, + vec![del(0, 2), del(9, 10), del(15, 16)] + ); + } + + #[test] + fn test_add_deletion() { + let mut repl = Replacements::default(); + repl.add_deletion(Span::new(1, 10)); + repl.add_deletion(Span::new(20, 30)); + assert_eq!(repl.list, vec![del(1, 10), del(20, 30)]); + + repl.add_deletion(Span::new(1, 30)); + assert_eq!(repl.list, vec![del(1, 30)]); + } + + #[test] + fn test_has() { + let repl = Replacements::new(vec![del(1, 10), del(20, 30)]); + + assert!(repl.has(Span::new(1, 10))); + assert!(repl.has(Span::new(4, 8))); + assert!(!repl.has(Span::new(1, 30))); + assert!(!repl.has(Span::new(20, 31))); + } + + #[test] + fn test_duplicated_spans() { + let mut repl = Replacements::default(); + assert!(repl.add_replacement(Span::new(1, 10), ReplacementValue::Del)); + assert!(!repl.add_replacement(Span::new(1, 10), ReplacementValue::Del)); + assert!(!repl.add_replacement(Span::new(3, 8), ReplacementValue::Del)); + assert_eq!(repl.list, vec![del(1, 10)]); + + // but with different value it should be added + assert!(repl.add_replacement(Span::new(1, 10), ReplacementValue::Undefined)); + assert_eq!(repl.list, vec![undefined(1, 10)]); + + // but if existing span is wider, it should be kept + assert!(!repl.add_replacement(Span::new(3, 8), ReplacementValue::Del)); + assert_eq!(repl.list, vec![undefined(1, 10)]); + } + + #[test] + fn test_apply() { + let source = "0123456789"; + let mut repl = Replacements::default(); + + repl.add_deletion(Span::new(0, 2)); + assert_eq!(repl.apply(source), "23456789"); + + repl.add_replacement(Span::new(3, 4), ReplacementValue::from_string("!")); + assert_eq!(repl.apply(source), "2!456789"); + + repl.add_replacement(Span::new(5, 5), ReplacementValue::from_string("insertion")); + assert_eq!(repl.apply(source), "2!4insertion56789"); + + repl.add_replacement(Span::new(0, 2), ReplacementValue::from_string("prefix")); + assert_eq!(repl.apply(source), "prefix2!4insertion56789"); + + repl.add_replacement(Span::new(8, 10), ReplacementValue::Span(Span::new(0, 2))); + assert_eq!(repl.apply(source), "prefix2!4insertion56701"); + } +}