diff --git a/.tool-versions b/.tool-versions index a6a7b22..b7e96e6 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ erlang 28.0.2 elixir 1.18.4-otp-28 -rust 1.89.0 +rust 1.97.1 pipx 1.8.0 diff --git a/lib/igniter_js/parsers/javascript/parser.ex b/lib/igniter_js/parsers/javascript/parser.ex index 46ef1af..9cdaa0b 100644 --- a/lib/igniter_js/parsers/javascript/parser.ex +++ b/lib/igniter_js/parsers/javascript/parser.ex @@ -68,14 +68,24 @@ defmodule IgniterJs.Parsers.Javascript.Parser do end @doc """ - Remove imports from the given file or content. it accepts a single module or a list of modules. - It returns a tuple. + Remove imports from the given file or content. It returns a tuple. + + The modules are given as one string, one per line. Each line may be either a bare module + specifier or a full import statement: ```elixir alias IgniterJs.Parsers.Javascript.Parser - Parser.remove_imports(js_content, "SomeModule") + + Parser.remove_imports(js_content, "topbar") + Parser.remove_imports(js_content, "../vendor/topbar") + Parser.remove_imports(js_content, ~s|import topbar from "../vendor/topbar";|) + Parser.remove_imports(js_content, "phoenix\\n../vendor/topbar") Parser.remove_imports("/path/to/file.js", "SomeModule", :path) ``` + + Matching is done on the module source, which is the text between the quotes, and not on + the local binding name. So given `import topbar from "../vendor/topbar";`, removing + `"../vendor/topbar"` works but removing `"topbar"` does not. """ def remove_imports(file_path_or_content, module, type \\ :content) @@ -130,13 +140,13 @@ defmodule IgniterJs.Parsers.Javascript.Parser do ```elixir alias IgniterJs.Parsers.Javascript.Parser - Parser.exist_live_socket?(js_content) - Parser.exist_live_socket?(js_content, :content) - Parser.exist_live_socket?("/path/to/file.js", :path) + Parser.var_exists?(js_content, "Hooks") + Parser.var_exists?(js_content, "Hooks", :content) + Parser.var_exists?("/path/to/file.js", "Hooks", :path) ``` """ - def var_exists?(file_path_or_content, type \\ :content) do - elem(exist_var(file_path_or_content, type), 0) == :ok + def var_exists?(file_path_or_content, var_name, type \\ :content) do + elem(exist_var(file_path_or_content, var_name, type), 0) == :ok end @doc """ @@ -227,6 +237,14 @@ defmodule IgniterJs.Parsers.Javascript.Parser do This function accepts either the content of the JavaScript file or the path to the file, and returns a tuple with the status, function atom, and the extracted data as a map. + On success the third element is a map of counts. When the path cannot be read it is a + reason string instead: + + ```elixir + {:ok, :statistics, %{functions: 1, classes: 0, debuggers: 0, imports: 2, trys: 0, throws: 0}} + {:error, :statistics, "Invalid file path or format."} + ``` + ## Examples ```elixir @@ -240,18 +258,22 @@ defmodule IgniterJs.Parsers.Javascript.Parser do ``` """ def statistics(file_path_or_content, type \\ :content) do - {status, fn_atom, {_, data}} = - call_nif_fn( - file_path_or_content, - __ENV__.function, - fn file_content -> - Native.statistics_from_ast_nif(file_content) - end, - type - ) - - converted = if is_map(data), do: Map.drop(data, [:__struct__]), else: data - {status, fn_atom, converted} + file_path_or_content + |> call_nif_fn( + __ENV__.function, + fn file_content -> + Native.statistics_from_ast_nif(file_content) + end, + type + ) + |> case do + {status, fn_atom, {_tag, data}} -> + converted = if is_map(data), do: Map.drop(data, [:__struct__]), else: data + {status, fn_atom, converted} + + {status, fn_atom, reason} -> + {status, fn_atom, reason} + end end @doc """ diff --git a/mix.exs b/mix.exs index 47575d6..58799ab 100644 --- a/mix.exs +++ b/mix.exs @@ -111,15 +111,15 @@ defmodule IgniterJs.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:rustler, "~> 0.36.2", optional: true}, + {:rustler, "~> 0.38.0", optional: true}, {:jason, "~> 1.4"}, - {:rustler_precompiled, "~> 0.8"}, - {:ex_doc, "~> 0.38", only: [:dev, :test], runtime: false}, + {:rustler_precompiled, "~> 0.9"}, + {:ex_doc, "~> 0.40", only: [:dev, :test], runtime: false}, {:ex_check, "~> 0.16", only: [:dev, :test]}, {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:sobelow, ">= 0.0.0", only: [:dev, :test], runtime: false}, - {:git_ops, "~> 2.9", only: [:dev, :test]}, + {:git_ops, "~> 2.10", only: [:dev, :test]}, {:mix_audit, ">= 0.0.0", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index dbc22a3..17a6e06 100644 --- a/mix.lock +++ b/mix.lock @@ -2,8 +2,8 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.46", "67607a0532e810c6f630a515c548d0b24949643f168cc556303bee4cf96105c7", [:mix], [], "hexpm", "9c44636e8a1c68c62f526b2dcd85d941dbbcee7ab82cf64ba06ce28bef8e89f5"}, + "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, "ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, @@ -12,7 +12,7 @@ "git_ops": {:hex, :git_ops, "2.10.0", "225780d8dcf9ef3393d26fa8d41d6a454a71149393c040e4b708c7c0b9c2b0f1", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.27 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "acd4a542eb425a58ce54505de69be704af3d0ef23c9b8cf9a3299d88e5d098ae"}, "hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, - "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, @@ -22,11 +22,10 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"}, - "rustler": {:hex, :rustler, "0.36.2", "6c2142f912166dfd364017ab2bf61242d4a5a3c88e7b872744642ae004b82501", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "93832a6dbc1166739a19cd0c25e110e4cf891f16795deb9361dfcae95f6c88fe"}, + "rustler": {:hex, :rustler, "0.38.0", "7a8906998ff0d28e3021c0a73264abcda719bda344b2e58307c6805b0f87c9b4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "704c03c1bf66be12b031c5a389347b91c81c5cb819a24b068b0de36fe4a5652a"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, - "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, - "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, } diff --git a/native/igniter_js/Cargo.lock b/native/igniter_js/Cargo.lock index 91f5762..7a0d9e5 100644 --- a/native/igniter_js/Cargo.lock +++ b/native/igniter_js/Cargo.lock @@ -50,12 +50,6 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "ascii" version = "1.1.0" @@ -64,9 +58,9 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "ast_node" -version = "3.0.3" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e2cddd48eafd883890770673b1971faceaf80a185445671abc3ea0c00593ee" +checksum = "b127110e8724bd19447ac72f31a66a9c06ac6b2b1372d81b6f5f0a2bddc6d1bb" dependencies = [ "quote", "swc_macros_common", @@ -94,16 +88,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "better_scoped_tls" version = "1.0.1" @@ -458,23 +442,11 @@ checksum = "87e8604d34b02180a58af1dbdaac166f1805f27f5370934142a3246f83870952" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ - "serde", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", + "serde_core", ] [[package]] @@ -572,17 +544,16 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" dependencies = [ "castaway", "cfg-if", "itoa", - "rustversion", - "ryu", "serde", "static_assertions", + "zmij", ] [[package]] @@ -626,22 +597,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "data-encoding" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" - -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "serde", - "uuid", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -655,9 +610,9 @@ dependencies = [ [[package]] name = "dragonbox_ecma" -version = "0.0.5" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d742b56656e8b14d63e7ea9806597b1849ae25412584c8adf78c0f67bd985e66" +checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf" [[package]] name = "drop_bomb" @@ -706,20 +661,14 @@ dependencies = [ [[package]] name = "from_variant" -version = "2.0.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308530a56b099da144ebc5d8e179f343ad928fa2b3558d1eb3db9af18d6eff43" +checksum = "e5ff35a391aef949120a0340d690269b3d9f63460a6106e99bd07b961f345ea9" dependencies = [ "swc_macros_common", "syn 2.0.98", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "gimli" version = "0.31.1" @@ -747,6 +696,12 @@ name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", ] @@ -757,16 +712,23 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hstr" -version = "2.0.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced1416104790052518d199e753d49a7d8130d476c664bc9e53f40cfecb8e615" +checksum = "76e2be4baa20c69a32de3b3b8ee67d7a7376591cc61f81ecc08fa43a1e36b897" dependencies = [ "hashbrown 0.14.5", "new_debug_unreachable", "once_cell", "rustc-hash 2.1.1", + "serde", "triomphe", ] @@ -909,12 +871,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - [[package]] name = "igniter_js" version = "0.4.11" @@ -1004,9 +960,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "json-strip-comments" @@ -1025,12 +981,12 @@ checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libloading" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" dependencies = [ "cfg-if", - "windows-targets", + "windows-link", ] [[package]] @@ -1051,9 +1007,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -1096,6 +1052,16 @@ dependencies = [ "serde", ] +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1114,6 +1080,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.36.7" @@ -1125,15 +1101,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" - -[[package]] -name = "outref" -version = "0.5.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "owo-colors" @@ -1143,23 +1113,25 @@ checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" [[package]] name = "oxc-miette" -version = "2.4.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31cfb121c9d3e0f9082856927f5cff9594279c91b544f4436e4bc971563caa60" +checksum = "2e0df30faa68797917ca4263e7a2f889ec829e4da2dcb3d6dc752f7a494180f3" dependencies = [ "cfg-if", + "memchr", "owo-colors", "oxc-miette-derive", "textwrap", "thiserror 2.0.16", - "unicode-width 0.2.0", + "unicode-segmentation", + "unicode-width 0.2.2", ] [[package]] name = "oxc-miette-derive" -version = "2.4.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6eabb57f935b454fbe0552ea0abaaf9eb0019b5fa05a7bbe7efd5bd8c765085" +checksum = "acc072d11d45ebe7801459b4e829184ba0934d68027fdc51d327335b53a95a49" dependencies = [ "proc-macro2", "quote", @@ -1168,13 +1140,12 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bdf7abfb88ad84430ccaf674d3fa463af5dceb0fd21ddaae83e7d2057668ce3" +checksum = "ae912912216c33ad0fb27a544160f3833a5e92a1216d7c4b9aa79a94b39aee98" dependencies = [ "allocator-api2", - "bumpalo", - "hashbrown 0.15.5", + "hashbrown 0.17.1", "oxc_data_structures", "oxc_estree", "rustc-hash 2.1.1", @@ -1183,37 +1154,39 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6a0d0ba1b1115575d35bfcd64f1458fe5db70fd988c08b742da223be2ea65f" +checksum = "6f0d567d1a93714e4b6b14eabe76c45581a25f418f47c297911a43fe4ee99615" dependencies = [ "bitflags", "oxc_allocator", "oxc_ast_macros", "oxc_data_structures", + "oxc_diagnostics", "oxc_estree", "oxc_regular_expression", "oxc_span", + "oxc_str", "oxc_syntax", ] [[package]] name = "oxc_ast_macros" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f299cae11166ffa2fe60a87df1de417f2ac05f21b7dffba173a004f667f321" +checksum = "a3fff81fa6fffffcf13826a6bc352e402a9197ac88ca286e2b09a0c538904708" dependencies = [ - "phf 0.13.1", + "phf 0.14.0", "proc-macro2", "quote", - "syn 2.0.98", + "syn 3.0.3", ] [[package]] name = "oxc_ast_visit" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b27334d066366a5d1289f0de12725be057ff1ac90e5b8fe6589d94d14c8a1fa" +checksum = "d13c8c77043e21d491840a6d52aa00f3ed1432662b096665b6ad702626f3ec5e" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1223,15 +1196,15 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39656fa64b09714c36416d954897e8fb0f42c462c3082fda22c8760282f55a15" +checksum = "2d7793b9a760ff426782915100e1f91b4a653873a5b681576214a6eb495d498a" [[package]] name = "oxc_diagnostics" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da029cea375d098fc97993afe96413563a55ebc80ff704b831e1dddea2e78911" +checksum = "a401193a5bc7f8acc2665763ea5876cad4a64e4e85c55133376620906e34b9e1" dependencies = [ "cow-utils", "oxc-miette", @@ -1240,24 +1213,25 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b868985647967b7b173c9681a6486691cf9c5d0fb4e64d114e65cc8a8978657a" +checksum = "9f099596aa0f5cb1aac154f7833abc02e0205aac19ce8b2bd21ee1e52973de61" dependencies = [ - "cow-utils", - "num-bigint", + "dragonbox_ecma", + "itoa", + "num-bigint 0.5.1", "num-traits", - "oxc_allocator", "oxc_ast", + "oxc_data_structures", "oxc_span", "oxc_syntax", ] [[package]] name = "oxc_estree" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c20498f0f4b7afec0a8b772174bf7736d79ff31a8e0073dd9081747801407c5" +checksum = "f2284782be0ac819aa56b815e6448e140e9f4f2c917917ac4ffa8bd7ccc35a3b" dependencies = [ "dragonbox_ecma", "itoa", @@ -1266,23 +1240,24 @@ dependencies = [ [[package]] name = "oxc_index" -version = "3.0.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa07b0cfa997730afed43705766ef27792873fdf5215b1391949fec678d2392" +checksum = "191884bee6c3744909a51acc7d78d4ae370d817b25875b10642f632327b6296e" dependencies = [ + "nonmax", "serde", ] [[package]] name = "oxc_parser" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df72e23adc7a481f60a1410fafc3833a5411c077271e2a5bb97083c93b274683" +checksum = "ff65aedf1d4364457427d461b827cc43544c5275f2693144a4fdb5fe5c26fbd9" dependencies = [ "bitflags", "cow-utils", "memchr", - "num-bigint", + "num-bigint 0.5.1", "num-traits", "oxc_allocator", "oxc_ast", @@ -1291,6 +1266,7 @@ dependencies = [ "oxc_ecmascript", "oxc_regular_expression", "oxc_span", + "oxc_str", "oxc_syntax", "rustc-hash 2.1.1", "seq-macro", @@ -1298,16 +1274,17 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97247d67a14e17dcb004ca57e59283b235e1a84813a2048e0585ca901a64eb99" +checksum = "69bc0cfbcd78e42d9aaa72eccb77e62b4b85b671f5d08ff8430aeea9816a8273" dependencies = [ "bitflags", "oxc_allocator", "oxc_ast_macros", "oxc_diagnostics", "oxc_span", - "phf 0.13.1", + "oxc_str", + "phf 0.14.0", "rustc-hash 2.1.1", "unicode-id-start", ] @@ -1334,23 +1311,37 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b70dfb43ba1c04ae09ba4c0e85dbb7af2260ff029458fe45a7f4e7d0384442c" +checksum = "890589be8c87e7c0f7a1f0331a9b8d362ebcd3e543125815daa942fd550b0121" dependencies = [ - "compact_str 0.9.0", + "compact_str 0.10.0", "oxc-miette", "oxc_allocator", "oxc_ast_macros", "oxc_estree", + "oxc_str", + "serde", +] + +[[package]] +name = "oxc_str" +version = "0.142.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f2b9a92f74a8231f21fccfb4eef937d266a7323d9597ccdcd74c7ad0f951b1" +dependencies = [ + "compact_str 0.10.0", + "hashbrown 0.17.1", + "oxc_allocator", + "oxc_estree", "serde", ] [[package]] name = "oxc_syntax" -version = "0.86.0" +version = "0.142.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5478a4c0b503ac61c09e5450a607517ad7f8dba36b7bea53db17d4b7221ef7df" +checksum = "e54e5c73453878e194df8d77d5cdc9ef525fec8ad25c547cc87162318eda6adb" dependencies = [ "bitflags", "cow-utils", @@ -1358,16 +1349,24 @@ dependencies = [ "nonmax", "oxc_allocator", "oxc_ast_macros", - "oxc_data_structures", "oxc_estree", "oxc_index", "oxc_span", - "phf 0.13.1", - "rustc-hash 2.1.1", + "oxc_str", + "phf 0.14.0", "serde", "unicode-id-start", ] +[[package]] +name = "par-core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96cbd21255b7fb29a5d51ef38a779b517a91abd59e2756c039583f43ef4c90f" +dependencies = [ + "once_cell", +] + [[package]] name = "parking_lot_core" version = "0.9.10" @@ -1383,9 +1382,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" @@ -1399,12 +1398,12 @@ dependencies = [ [[package]] name = "phf" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", + "phf_macros 0.14.0", + "phf_shared 0.14.0", "serde", ] @@ -1420,12 +1419,12 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" dependencies = [ "fastrand", - "phf_shared 0.13.1", + "phf_shared 0.14.0", ] [[package]] @@ -1443,12 +1442,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator 0.14.0", + "phf_shared 0.14.0", "proc-macro2", "quote", "syn 2.0.98", @@ -1465,9 +1464,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" dependencies = [ "siphasher 1.0.1", ] @@ -1528,12 +1527,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.6" @@ -1613,11 +1606,12 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustler" -version = "0.36.2" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3fe55230a9c379733dd38ee67d4072fa5c558b2e22b76b0e7f924390456e003" +checksum = "60ac8495cb6091c1f8bd0c3bb816cd4e6f0a073f396d77eb602cb7f0615212de" dependencies = [ "inventory", + "libc", "libloading", "regex-lite", "rustler_codegen", @@ -1625,9 +1619,9 @@ dependencies = [ [[package]] name = "rustler_codegen" -version = "0.36.2" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3b8de901ae61418e2036245d28e41ef58080d04f40b68430471ae36a4e84ed" +checksum = "780a2a6b7e3cfac35820473f85cec1cd84d1bdd37ebcb4e24fb126ef0f074838" dependencies = [ "heck", "inventory", @@ -1648,12 +1642,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" -[[package]] -name = "ryu-js" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" - [[package]] name = "schemars" version = "0.8.21" @@ -1698,22 +1686,32 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 3.0.3", ] [[package]] @@ -1729,15 +1727,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap 2.7.1", "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -1780,17 +1779,6 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - [[package]] name = "smawk" version = "0.3.2" @@ -1835,9 +1823,9 @@ dependencies = [ [[package]] name = "swc_allocator" -version = "4.0.1" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d7eefd2c8b228a8c73056482b2ae4b3a1071fbe07638e3b55ceca8570cc48bb" +checksum = "eb41c2f41afa7357a86109f7e058f6b140ab415b8cd196c1a7b54f2703c85417" dependencies = [ "allocator-api2", "bumpalo", @@ -1847,9 +1835,9 @@ dependencies = [ [[package]] name = "swc_atoms" -version = "7.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3500dcf04c84606b38464561edc5e46f5132201cb3e23cf9613ed4033d6b1bb2" +checksum = "c70a493080ceb12dabddb96905a3ddac679fbe9c71ddc81a2a769126e8cde7c4" dependencies = [ "hstr", "once_cell", @@ -1858,9 +1846,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "14.0.3" +version = "24.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63fdb58d278e7cd625f671e5371b3e6c0eab56c6e2a995a6f70dd0f7725255d4" +checksum = "c5d7b9e59d7284543c4e6a2a9e264fee6443cdf33fa9101c113958f078df12e2" dependencies = [ "anyhow", "ast_node", @@ -1868,8 +1856,7 @@ dependencies = [ "bytes-str", "either", "from_variant", - "new_debug_unreachable", - "num-bigint", + "num-bigint 0.4.6", "once_cell", "rustc-hash 2.1.1", "serde", @@ -1879,19 +1866,19 @@ dependencies = [ "swc_visit", "termcolor", "tracing", - "unicode-width 0.1.14", + "unicode-width 0.2.2", "url", ] [[package]] name = "swc_ecma_ast" -version = "15.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c25af97d53cf8aab66a6c68f3418663313fc969ad267fc2a4d19402c329be1" +checksum = "0aa75407fc0d30682819c3e88ce651c265d67a5d920a29882d647cdcd8574d66" dependencies = [ "bitflags", "is-macro", - "num-bigint", + "num-bigint 0.4.6", "once_cell", "phf 0.11.3", "rustc-hash 2.1.1", @@ -1904,25 +1891,25 @@ dependencies = [ [[package]] name = "swc_ecma_codegen" -version = "17.0.0" +version = "30.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91da8222bd2e868a6977ef402b3ca5c29a41d18cd84772441d9e06ec95ded1f" +checksum = "5a37f2ef6664fadfbc9ae27ee01375c4847ae0c68a364c9647189cb2e9102ff9" dependencies = [ "ascii", "compact_str 0.7.1", + "dragonbox_ecma", "memchr", - "num-bigint", + "num-bigint 0.4.6", "once_cell", "regex", "rustc-hash 2.1.1", - "ryu-js", "serde", "swc_allocator", "swc_atoms", "swc_common", "swc_ecma_ast", "swc_ecma_codegen_macros", - "swc_sourcemap", + "swc_ecma_utils", "tracing", ] @@ -1938,21 +1925,19 @@ dependencies = [ ] [[package]] -name = "swc_ecma_lexer" -version = "23.0.1" +name = "swc_ecma_parser" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c3bd958a5a67e2cc3f74abdd41fda688e54e7a25b866569260ef7018b67972" +checksum = "355c7f0558d6085cb4e8641f19f53cb444edd267c6d9f74fd356d1867fe52f02" dependencies = [ - "arrayvec", "bitflags", + "compact_str 0.7.1", "either", - "num-bigint", + "num-bigint 0.4.6", "phf 0.11.3", "rustc-hash 2.1.1", "seq-macro", "serde", - "smallvec", - "smartstring", "stacker", "swc_atoms", "swc_common", @@ -1961,29 +1946,32 @@ dependencies = [ ] [[package]] -name = "swc_ecma_parser" -version = "24.0.0" +name = "swc_ecma_utils" +version = "33.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c0b41d7e86acb8abc1e75b39163a1dffd88f75b69d8f89a199dfc416bb46d6" +checksum = "d893f42a056dab249b5830bd204db4aae8143bae12402269c13fb2f3b6dbad9e" dependencies = [ - "either", - "num-bigint", - "serde", + "dragonbox_ecma", + "indexmap 2.7.1", + "num_cpus", + "once_cell", + "par-core", + "rustc-hash 2.1.1", "swc_atoms", "swc_common", "swc_ecma_ast", - "swc_ecma_lexer", + "swc_ecma_visit", "tracing", ] [[package]] name = "swc_ecma_visit" -version = "15.0.0" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a579aa8f9e212af521588df720ccead079c09fe5c8f61007cf724324aed3a0" +checksum = "53419a8907b76977f2446ee2121c2a6d9761d432771e3f5a724893f3ea1a00db" dependencies = [ "new_debug_unreachable", - "num-bigint", + "num-bigint 0.4.6", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -2013,25 +2001,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "swc_sourcemap" -version = "9.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de08ef00f816acdd1a58ee8a81c0e1a59eefef2093aefe5611f256fa6b64c4d7" -dependencies = [ - "base64-simd", - "bitvec", - "bytes-str", - "data-encoding", - "debugid", - "if_chain", - "rustc-hash 2.1.1", - "serde", - "serde_json", - "unicode-id-start", - "url", -] - [[package]] name = "swc_visit" version = "2.0.1" @@ -2064,6 +2033,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -2075,12 +2055,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "termcolor" version = "1.4.1" @@ -2098,7 +2072,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.2.0", + "unicode-width 0.2.2", ] [[package]] @@ -2230,9 +2204,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "url" @@ -2257,24 +2231,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "uuid" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1" - [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - [[package]] name = "winapi-util" version = "0.1.9" @@ -2284,6 +2246,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" @@ -2369,15 +2337,6 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "yoke" version = "0.7.5" @@ -2464,3 +2423,9 @@ dependencies = [ "quote", "syn 2.0.98", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/native/igniter_js/Cargo.toml b/native/igniter_js/Cargo.toml index 8d69175..22a39f4 100644 --- a/native/igniter_js/Cargo.toml +++ b/native/igniter_js/Cargo.toml @@ -14,12 +14,12 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -swc_common = { version = "14.0.3", features = ["tty-emitter"] } -swc_ecma_parser = { version = "24.0.0" } -swc_ecma_visit = "15.0.0" -swc_ecma_ast = "15.0.0" -swc_ecma_codegen = "17.0.0" -rustler = "=0.36.2" +swc_common = { version = "24.0.0", features = ["tty-emitter"] } +swc_ecma_parser = { version = "43.0.0" } +swc_ecma_visit = "27.0.0" +swc_ecma_ast = "27.0.0" +swc_ecma_codegen = "30.0.0" +rustler = "=0.38.0" biome_console = "=0.5.7" biome_deserialize = "=0.5.7" @@ -45,11 +45,11 @@ biome_css_formatter = "=0.5.7" biome_css_parser = "=0.5.7" biome_css_syntax = "=0.5.7" biome_css_factory = "=0.5.7" -serde = "1.0.219" -serde_json = "1.0.143" -oxc_allocator = "0.86.0" -oxc_ast = "0.86.0" -oxc_parser = "0.86.0" -oxc_span = "0.86.0" -oxc_ast_visit = { version = "0.86.0", features = ["serialize"] } -oxc_diagnostics = "0.86.0" +serde = "1.0.229" +serde_json = "1.0.151" +oxc_allocator = "0.142.0" +oxc_ast = "0.142.0" +oxc_parser = "0.142.0" +oxc_span = "0.142.0" +oxc_ast_visit = { version = "0.142.0", features = ["serialize"] } +oxc_diagnostics = "0.142.0" diff --git a/native/igniter_js/src/parsers/css/formatter.rs b/native/igniter_js/src/parsers/css/formatter.rs index 2195107..c0a8971 100644 --- a/native/igniter_js/src/parsers/css/formatter.rs +++ b/native/igniter_js/src/parsers/css/formatter.rs @@ -78,9 +78,46 @@ h1 { font-size: 20px; } "#; - assert_eq!(is_formatted(css_unformatted).unwrap(), false); + assert!(!is_formatted(css_unformatted).unwrap()); let formatted = format(css_formatted).unwrap(); - assert_eq!(is_formatted(&formatted).unwrap(), true); + assert!(is_formatted(&formatted).unwrap()); + } + + #[test] + fn test_format_rejects_syntax_errors() { + assert!(format("body { color").is_err()); + assert!(format("@@@").is_err()); + } + + #[test] + fn test_is_formatted_propagates_syntax_errors() { + assert!(is_formatted("body { color").is_err()); + } + + #[test] + fn test_format_is_idempotent() { + let once = format("body{color:red;}").unwrap(); + let twice = format(&once).unwrap(); + assert_eq!(once, twice); + } + + #[test] + fn test_format_uses_two_space_indent() { + assert_eq!( + format("body{color:red;}").unwrap(), + "body {\n color: red;\n}\n" + ); + } + + #[test] + fn test_format_preserves_comments() { + let output = format("/* keep me */\nbody{color:red;}").unwrap(); + assert!(output.contains("/* keep me */"), "got: {output}"); + } + + #[test] + fn test_format_empty_input() { + assert_eq!(format("").unwrap(), ""); } } diff --git a/native/igniter_js/src/parsers/javascript/ast.rs b/native/igniter_js/src/parsers/javascript/ast.rs index 2965132..590d136 100644 --- a/native/igniter_js/src/parsers/javascript/ast.rs +++ b/native/igniter_js/src/parsers/javascript/ast.rs @@ -64,6 +64,10 @@ struct ASTVisitImport<'a> { duplicate_imports: Vec, none_duplicate_imports: Vec, operation: Operation, + /// Set when `code` cannot be parsed as JavaScript. `VisitMut` methods + /// return `()`, so callers must check this once the visit completes and + /// surface it as an error. + parse_error: Option, } impl Default for ASTVisitImport<'_> { @@ -73,24 +77,63 @@ impl Default for ASTVisitImport<'_> { duplicate_imports: Vec::new(), none_duplicate_imports: Vec::new(), operation: Operation::Edit, + parse_error: None, } } } +/// Returned when the module/import argument is not valid JavaScript. The +/// argument is parsed as source, so a bare filesystem path such as +/// `../vendor/topbar` is a syntax error. +const INVALID_ARGUMENT_MESSAGE: &str = + "The given module or import argument could not be interpreted. Provide a full import \ + statement (`import topbar from \"../vendor/topbar\";`), or, when removing imports, \ + a bare module specifier (`../vendor/topbar`)."; + +/// Collects the module sources a removal request refers to. +/// +/// The argument is first parsed as JavaScript. If it contains import +/// declarations, only the sources those import from are used, which keeps +/// multi-line import statements intact. Only when the argument holds no import +/// declaration at all is each non-empty line taken literally as a module +/// specifier, which is what lets a bare `topbar` or a path such as +/// `../vendor/topbar` work even though neither is a valid import statement. +fn removal_targets(code: &str) -> Vec { + if let Ok((parsed, _comments, _cm)) = parse(code) { + let sources: Vec = parsed + .body + .iter() + .filter_map(|item| match item { + ModuleItem::ModuleDecl(ModuleDecl::Import(decl)) => { + Some(decl.src.value.to_string_lossy().to_string()) + } + _ => None, + }) + .collect(); + + if !sources.is_empty() { + return sources; + } + } + + code.lines() + .map(|line| line.trim().trim_end_matches(';').trim()) + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect() +} + impl VisitMut for ASTVisitImport<'_> { fn visit_mut_module_items(&mut self, items: &mut Vec) { - // We are using it to delete imports - let (imports, _comments, _cm) = parse(self.code).expect("Failed to parse imports"); - if matches!(self.operation, Operation::Delete) { + let targets = removal_targets(self.code); let mut indices_to_remove = vec![]; for (index, item) in items.iter().enumerate() { if let ModuleItem::ModuleDecl(ModuleDecl::Import(existing_import)) = item { - if imports.body.iter().any(|import| { - matches!(import, ModuleItem::ModuleDecl(ModuleDecl::Import(new_import)) - if new_import.src.value == existing_import.src.value) - }) { + let source = existing_import.src.value.to_string_lossy().to_string(); + + if targets.contains(&source) { indices_to_remove.push(index); } } @@ -105,8 +148,19 @@ impl VisitMut for ASTVisitImport<'_> { } fn visit_mut_module(&mut self, module: &mut Module) { + if !matches!(self.operation, Operation::Add | Operation::Read) { + module.visit_mut_children_with(self); + return; + } + // We are using it to add imports and know it is duplicated or not - let (imports, _comments, _cm) = parse(self.code).expect("Failed to parse imports"); + let imports = match parse(self.code) { + Ok((imports, _comments, _cm)) => imports, + Err(_) => { + self.parse_error = Some(INVALID_ARGUMENT_MESSAGE.to_string()); + return; + } + }; for import in imports.body { if !is_duplicate_import(&import, &module.body) { @@ -120,7 +174,7 @@ impl VisitMut for ASTVisitImport<'_> { for imp in import.as_module_decl().iter() { if let ModuleDecl::Import(import_decl) = imp { - let src_value = import_decl.src.value.to_string(); + let src_value = import_decl.src.value.to_string_lossy().to_string(); if !self.none_duplicate_imports.contains(&src_value) { self.none_duplicate_imports.push(src_value); } @@ -136,7 +190,7 @@ impl VisitMut for ASTVisitImport<'_> { } else if matches!(self.operation, Operation::Read) { if let ModuleItem::ModuleDecl(ModuleDecl::Import(new_import_decl)) = import { self.duplicate_imports - .push(new_import_decl.src.value.to_string()); + .push(new_import_decl.src.value.to_string_lossy().to_string()); } } } @@ -166,6 +220,10 @@ pub fn is_module_imported_from_ast(file_content: &str, module_name: &str) -> Res let _output = code_gen_from_ast_vist(file_content, &mut import_visitor); + if import_visitor.parse_error.is_some() { + return Err(false); + } + if import_visitor.none_duplicate_imports.is_empty() && import_visitor.duplicate_imports.is_empty() { @@ -201,7 +259,12 @@ pub fn insert_import_to_ast(file_content: &str, import_lines: &str) -> Result Err(error), + None => Ok(output), + } } /// Removes specified import statements from JavaScript source code. @@ -212,7 +275,11 @@ pub fn insert_import_to_ast(file_content: &str, import_lines: &str) -> Result Result Err(error), + None => Ok(output), + } } // ################################################################################### @@ -490,7 +562,10 @@ pub fn extend_var_object_property_by_names_to_ast<'a>( /// assert_eq!(result, Err(false)); /// ``` pub fn contains_variable_from_ast(file_content: &str, variable_name: &str) -> Result { - let (module, _, _) = parse(file_content).expect("Failed to parse imports"); + let (module, _, _) = match parse(file_content) { + Ok(result) => result, + Err(_) => return Err(false), + }; for item in &module.body { if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(var_decl))) = item { @@ -615,6 +690,105 @@ mod tests { use super::*; + /// Adding and querying still need real import statements, so a bare + /// specifier must surface as an error rather than a panic. + #[test] + fn test_unparseable_argument_returns_error_instead_of_panicking() { + let code = "import topbar from \"../vendor/topbar\";\nlet Hooks = {};\n"; + let invalid = "../vendor/topbar"; + + let result = insert_import_to_ast(code, invalid); + assert!( + result.is_err(), + "insert_import_to_ast should error, got {:?}", + result + ); + + let result = is_module_imported_from_ast(code, invalid); + assert_eq!( + result, + Err(false), + "is_module_imported_from_ast should report not-imported" + ); + } + + /// The same guarantee for unparseable source rather than an unparseable + /// argument. + #[test] + fn test_unparseable_source_returns_error_instead_of_panicking() { + let broken = "let x = ;;; import * from;"; + + assert_eq!(contains_variable_from_ast(broken, "x"), Err(false)); + assert!(remove_import_from_ast(broken, "topbar").is_err()); + } + + const TWO_IMPORTS: &str = + "import { Socket } from \"phoenix\";\nimport topbar from \"../vendor/topbar\";\nlet Hooks = {};\n"; + + #[test] + fn test_remove_import_by_bare_module_specifier() { + let result = remove_import_from_ast(TWO_IMPORTS, "../vendor/topbar").unwrap(); + + assert!(!result.contains("topbar"), "got: {result}"); + assert!(result.contains("phoenix"), "got: {result}"); + assert!(result.contains("let Hooks"), "got: {result}"); + } + + #[test] + fn test_remove_import_by_full_statement_still_works() { + let result = + remove_import_from_ast(TWO_IMPORTS, "import topbar from \"../vendor/topbar\";") + .unwrap(); + + assert!(!result.contains("topbar"), "got: {result}"); + assert!(result.contains("phoenix"), "got: {result}"); + } + + #[test] + fn test_remove_several_bare_specifiers_one_per_line() { + let result = remove_import_from_ast(TWO_IMPORTS, "phoenix\n../vendor/topbar").unwrap(); + + assert!(!result.contains("topbar"), "got: {result}"); + assert!(!result.contains("phoenix"), "got: {result}"); + assert!(result.contains("let Hooks"), "got: {result}"); + } + + /// A multi-line import statement must not have its inner lines mistaken for + /// bare module specifiers. + #[test] + fn test_multiline_import_statement_does_not_leak_literal_targets() { + let code = "import x from \"foo\";\nimport { foo } from \"module-name\";\n"; + let argument = "import {\n foo\n} from \"module-name\";"; + + let result = remove_import_from_ast(code, argument).unwrap(); + + assert!(result.contains("import x from \"foo\";"), "got: {result}"); + assert!(!result.contains("module-name"), "got: {result}"); + } + + /// Matching is on the module source, so the local binding name is not a + /// removal key. + #[test] + fn test_remove_import_does_not_match_local_binding_name() { + let result = remove_import_from_ast(TWO_IMPORTS, "topbar").unwrap(); + + assert!(result.contains("../vendor/topbar"), "got: {result}"); + } + + #[test] + fn test_remove_import_of_an_absent_module_is_a_no_op() { + let result = remove_import_from_ast(TWO_IMPORTS, "not-imported").unwrap(); + + assert_eq!(result.trim(), TWO_IMPORTS.trim()); + } + + #[test] + fn test_remove_import_with_a_blank_argument_is_a_no_op() { + let result = remove_import_from_ast(TWO_IMPORTS, " ").unwrap(); + + assert_eq!(result.trim(), TWO_IMPORTS.trim()); + } + #[test] fn test_is_module_imported_from_ast() { let code = r#" @@ -839,8 +1013,7 @@ mod tests { println!("{:#?}", result.unwrap()) } - #[cfg(test)] - mod tests { + mod index_operations { use super::*; #[test] diff --git a/native/igniter_js/src/parsers/javascript/ast_json.rs b/native/igniter_js/src/parsers/javascript/ast_json.rs index f8711b3..d1c064c 100644 --- a/native/igniter_js/src/parsers/javascript/ast_json.rs +++ b/native/igniter_js/src/parsers/javascript/ast_json.rs @@ -29,9 +29,12 @@ use serde_json::json; /// /// # Output Structure /// The returned JSON contains: -/// * `"program"` - The parsed AST in ESTree format. -/// * `"comments"` - Extracted comments from the source code. -/// * `"errors"` - A list of syntax errors with details. +/// * `"program"` - The parsed AST in ESTree format. Every node carries `range` +/// (`[start, end]`) alongside `start`/`end`, and TypeScript fields are included. +/// * `"comments"` - Extracted comments from the source code, each tagged `"Line"` +/// or `"Block"`. Offsets are UTF-16, not UTF-8 bytes. +/// * `"errors"` - A list of syntax errors with details. A diagnostic with no +/// labels serializes `"labels"` as `null` rather than `[]`. /// /// # Example /// ```rust @@ -55,7 +58,7 @@ pub fn convert_ast_to_estree(source_text: &str) -> Result { .parse(); let errors = parser_return - .errors + .diagnostics .into_iter() .map(|e| { let severity = match e.severity { @@ -66,23 +69,27 @@ pub fn convert_ast_to_estree(source_text: &str) -> Result { let help = e.help.as_ref().map(|h| h.to_string()); - let labels = e.labels.as_ref().map(|labels| { - labels - .iter() - .map(|label| { - let span = label.inner(); - let start = span.offset(); - let end = start + span.len(); - - json!({ - "start": start, - "end": end, - "label": label.label().map(|s| s.to_string()), - "primary": label.primary() + let labels = if e.labels.is_empty() { + None + } else { + Some( + e.labels + .iter() + .map(|label| { + let span = label.inner(); + let start = span.offset(); + let end = start + span.len(); + + json!({ + "start": start, + "end": end, + "label": label.label().map(|s| s.to_string()), + "primary": label.primary() + }) }) - }) - .collect::>() - }); + .collect::>(), + ) + }; let code = e.code.to_string(); let url = e.url.as_ref().map(|u| u.to_string()); @@ -119,7 +126,7 @@ pub fn convert_ast_to_estree(source_text: &str) -> Result { }) }) .collect(); - let estree_json = program.to_pretty_estree_ts_json(true); + let estree_json = program.to_pretty_estree_json(true, true); let full_json = json!({ "program": serde_json::from_str::(&estree_json).unwrap_or(json!({})), @@ -154,4 +161,105 @@ mod tests { println!("{}", json_output); assert!(is_valid_json(&json_output)); } + + fn parse_to_value(code: &str) -> Value { + serde_json::from_str(&convert_ast_to_estree(code).expect("conversion must succeed")) + .expect("output must be valid JSON") + } + + #[test] + fn test_top_level_shape() { + let value = parse_to_value("const a = 1;"); + + assert!(value.get("program").is_some()); + assert!(value.get("comments").is_some()); + assert!(value.get("errors").is_some()); + assert_eq!(value["program"]["type"], "Program"); + } + + #[test] + fn test_valid_source_has_no_errors() { + let value = parse_to_value("const a = 1;"); + assert_eq!(value["errors"].as_array().unwrap().len(), 0); + } + + /// Syntax errors are reported in the `errors` array rather than failing the + /// whole conversion. + #[test] + fn test_syntax_errors_are_reported_not_raised() { + let value = parse_to_value("const a = ;"); + let errors = value["errors"].as_array().unwrap(); + + assert!(!errors.is_empty()); + assert_eq!(errors[0]["severity"], "Error"); + assert!(errors[0]["message"].is_string()); + } + + /// Diagnostics without labels serialize as `null`, not `[]`. + #[test] + fn test_label_less_diagnostics_serialize_as_null() { + let value = parse_to_value("const a = ;"); + + for error in value["errors"].as_array().unwrap() { + let labels = &error["labels"]; + assert!( + labels.is_null() || labels.is_array(), + "labels must be null or an array, got {labels}" + ); + } + } + + #[test] + fn test_comments_are_captured_with_kind_and_span() { + let value = parse_to_value("const a = 1; /* block */ // line"); + let comments = value["comments"].as_array().unwrap(); + + let kinds: Vec<_> = comments + .iter() + .map(|c| c["type"].as_str().unwrap()) + .collect(); + assert_eq!(kinds, vec!["Block", "Line"]); + + for comment in comments { + assert!(comment["start"].as_u64() < comment["end"].as_u64()); + assert!(comment["value"].is_string()); + } + } + + /// `ranges` is enabled, so every node carries `range` next to `start`/`end`. + #[test] + fn test_nodes_carry_range() { + let value = parse_to_value("const a = 1;"); + let program = &value["program"]; + + let range = program["range"] + .as_array() + .expect("program must have a range"); + assert_eq!(range[0], program["start"]); + assert_eq!(range[1], program["end"]); + + let node = &program["body"][0]; + assert_eq!(node["type"], "VariableDeclaration"); + assert!(node["range"].is_array()); + } + + #[test] + fn test_empty_source_still_produces_a_program() { + let value = parse_to_value(""); + + assert_eq!(value["program"]["type"], "Program"); + assert_eq!(value["program"]["body"].as_array().unwrap().len(), 0); + assert_eq!(value["errors"].as_array().unwrap().len(), 0); + } + + /// Multi-byte characters shift UTF-8 offsets, which are converted to the + /// UTF-16 offsets ESTree consumers expect. + #[test] + fn test_spans_are_converted_to_utf16_offsets() { + let value = parse_to_value("const emoji = \"😀\"; // tail"); + let comments = value["comments"].as_array().unwrap(); + + assert_eq!(comments.len(), 1); + assert_eq!(comments[0]["start"].as_u64().unwrap(), 20); + } } diff --git a/native/igniter_js/src/parsers/javascript/formatter.rs b/native/igniter_js/src/parsers/javascript/formatter.rs index 544593c..22beaa5 100644 --- a/native/igniter_js/src/parsers/javascript/formatter.rs +++ b/native/igniter_js/src/parsers/javascript/formatter.rs @@ -168,9 +168,38 @@ mod tests { let js_code_formatted = r#"function test() { console.log("hello world"); }"#; - assert_eq!(is_formatted(js_code_unformatted).unwrap(), false); + assert!(!is_formatted(js_code_unformatted).unwrap()); let formatted = format(js_code_formatted).unwrap(); - assert_eq!(is_formatted(&formatted).unwrap(), true); + assert!(is_formatted(&formatted).unwrap()); + } + + #[test] + fn test_format_rejects_syntax_errors() { + assert!(format("function {{{").is_err()); + assert!(format("const a = ;").is_err()); + } + + #[test] + fn test_is_formatted_propagates_syntax_errors() { + assert!(is_formatted("function {{{").is_err()); + } + + #[test] + fn test_format_is_idempotent() { + let once = format("const a=1;const b =2;").unwrap(); + let twice = format(&once).unwrap(); + assert_eq!(once, twice); + } + + #[test] + fn test_format_uses_two_space_indent() { + let output = format("function a(){return 1;}").unwrap(); + assert!(output.contains("\n return 1;"), "got: {output}"); + } + + #[test] + fn test_format_empty_input() { + assert_eq!(format("").unwrap(), ""); } } diff --git a/native/igniter_js/src/parsers/javascript/helpers.rs b/native/igniter_js/src/parsers/javascript/helpers.rs index f7486e5..3b508e5 100644 --- a/native/igniter_js/src/parsers/javascript/helpers.rs +++ b/native/igniter_js/src/parsers/javascript/helpers.rs @@ -137,3 +137,123 @@ fn specifier_equals(new_spec: &ImportSpecifier, existing_spec: &ImportSpecifier) pub fn replace_four_spaces_with_tab(input: &str) -> String { input.replace(" ", "\t") } + +#[cfg(test)] +mod tests { + use super::*; + + fn items(code: &str) -> Vec { + parse(code).expect("test fixture must parse").0.body + } + + fn first_item(code: &str) -> ModuleItem { + items(code).into_iter().next().expect("expected one item") + } + + #[test] + fn parse_accepts_valid_source() { + assert!(parse("const a = 1;").is_ok()); + assert!(parse("").is_ok()); + } + + #[test] + fn parse_rejects_invalid_source() { + match parse("const a = ;") { + Err(reason) => assert_eq!(reason, "Failed to parse module"), + Ok(_) => panic!("expected a parse error"), + } + + assert!(parse("import * from;").is_err()); + } + + #[test] + fn code_gen_reports_unparseable_source() { + struct Noop; + impl VisitMut for Noop {} + + assert!(code_gen_from_ast_vist("const a = ;", Noop).is_err()); + assert!(code_gen_from_ast_vist("const a = 1;", Noop).is_ok()); + } + + #[test] + fn code_gen_preserves_comments() { + struct Noop; + impl VisitMut for Noop {} + + let output = code_gen_from_ast_vist("// keep me\nconst a = 1;", Noop).unwrap(); + assert!(output.contains("// keep me"), "got: {output}"); + } + + #[test] + fn duplicate_named_import_is_detected() { + let body = items(r#"import { Socket } from "phoenix";"#); + let candidate = first_item(r#"import { Socket } from "phoenix";"#); + + assert!(is_duplicate_import(&candidate, &body)); + } + + #[test] + fn duplicate_default_import_is_detected() { + let body = items(r#"import topbar from "topbar";"#); + let candidate = first_item(r#"import topbar from "topbar";"#); + + assert!(is_duplicate_import(&candidate, &body)); + } + + #[test] + fn duplicate_namespace_import_is_detected() { + let body = items(r#"import * as All from "mod";"#); + let candidate = first_item(r#"import * as All from "mod";"#); + + assert!(is_duplicate_import(&candidate, &body)); + } + + #[test] + fn same_source_with_a_new_specifier_is_not_duplicate() { + let body = items(r#"import { Socket } from "phoenix";"#); + let candidate = first_item(r#"import { Socket, LiveSocket } from "phoenix";"#); + + assert!(!is_duplicate_import(&candidate, &body)); + } + + #[test] + fn different_source_is_not_duplicate() { + let body = items(r#"import { Socket } from "phoenix";"#); + let candidate = first_item(r#"import { Socket } from "other";"#); + + assert!(!is_duplicate_import(&candidate, &body)); + } + + /// Specifier kinds must match, so a default and a named import that share a + /// local name are still distinct. + #[test] + fn default_and_named_specifiers_are_distinct() { + let body = items(r#"import { Socket } from "phoenix";"#); + let candidate = first_item(r#"import Socket from "phoenix";"#); + + assert!(!is_duplicate_import(&candidate, &body)); + } + + #[test] + fn non_import_items_are_never_duplicates() { + let body = items(r#"import { Socket } from "phoenix";"#); + let candidate = first_item("const a = 1;"); + + assert!(!is_duplicate_import(&candidate, &body)); + } + + #[test] + fn empty_body_has_no_duplicates() { + let candidate = first_item(r#"import { Socket } from "phoenix";"#); + + assert!(!is_duplicate_import(&candidate, &[])); + } + + #[test] + fn replace_four_spaces_with_tab_converts_indentation() { + assert_eq!(replace_four_spaces_with_tab(" a"), "\ta"); + assert_eq!(replace_four_spaces_with_tab(" a"), "\t\ta"); + assert_eq!(replace_four_spaces_with_tab(" a"), " a"); + assert_eq!(replace_four_spaces_with_tab(""), ""); + } +} diff --git a/native/igniter_js/src/parsers/javascript/phoenix.rs b/native/igniter_js/src/parsers/javascript/phoenix.rs index 6fff0bc..bb0afd0 100644 --- a/native/igniter_js/src/parsers/javascript/phoenix.rs +++ b/native/igniter_js/src/parsers/javascript/phoenix.rs @@ -293,7 +293,10 @@ pub fn remove_objects_of_hooks_from_ast( ) -> Result { let mut hook_extender = HookExtender::new("liveSocket", vec![]); - let (mut module, comments, cm) = parse(file_content).expect("Failed to parse imports"); + let (mut module, comments, cm) = match parse(file_content) { + Ok(result) => result, + Err(_) => return Err("Failed to parse JavaScript content".to_string()), + }; module.visit_mut_with(&mut hook_extender); @@ -330,6 +333,15 @@ pub fn remove_objects_of_hooks_from_ast( mod tests { use super::*; + /// Unparseable source must surface as an error. + #[test] + fn test_remove_objects_of_hooks_from_unparseable_source() { + let broken = "let liveSocket = new LiveSocket(;;;"; + + let result = remove_objects_of_hooks_from_ast(broken, vec!["SomeHook"]); + assert!(result.is_err(), "expected an error, got {:?}", result); + } + #[test] fn test_extend_hook_object_to_ast() { let code = r#" @@ -505,9 +517,18 @@ mod tests { fn test_extend_hooks_with_const_let_var_declarations() { // Test with different variable declaration types let test_cases = vec![ - ("const", r#"const liveSocket = new LiveSocket("/live", Socket, { hooks: {} });"#), - ("let", r#"let liveSocket = new LiveSocket("/live", Socket, { hooks: {} });"#), - ("var", r#"var liveSocket = new LiveSocket("/live", Socket, { hooks: {} });"#), + ( + "const", + r#"const liveSocket = new LiveSocket("/live", Socket, { hooks: {} });"#, + ), + ( + "let", + r#"let liveSocket = new LiveSocket("/live", Socket, { hooks: {} });"#, + ), + ( + "var", + r#"var liveSocket = new LiveSocket("/live", Socket, { hooks: {} });"#, + ), ]; for (decl_type, code) in test_cases { diff --git a/test/helpers_test.exs b/test/helpers_test.exs new file mode 100644 index 0000000..9a48630 --- /dev/null +++ b/test/helpers_test.exs @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: 2024 igniter_js contributors +# +# SPDX-License-Identifier: MIT + +defmodule IgniterJSTest.HelpersTest do + use ExUnit.Case + require IgniterJs.Helpers + alias IgniterJs.Helpers + + @valid_app_js "test/assets/validApp.js" + + setup do + tmp = Path.join(System.tmp_dir!(), "igniter_js_helpers_#{System.unique_integer([:positive])}") + File.mkdir_p!(tmp) + on_exit(fn -> File.rm_rf(tmp) end) + {:ok, tmp: tmp} + end + + describe "read_and_validate_file/1" do + test "reads an existing .js file" do + assert {:ok, content} = Helpers.read_and_validate_file(@valid_app_js) + assert content =~ "liveSocket" + end + + test "accepts the .ts extension", %{tmp: tmp} do + path = Path.join(tmp, "sample.ts") + File.write!(path, "const answer: number = 42;") + + assert {:ok, "const answer: number = 42;"} = Helpers.read_and_validate_file(path) + end + + test "rejects a file that does not exist" do + assert {:error, "Invalid file path or format."} = + Helpers.read_and_validate_file("definitely/missing.js") + end + + test "rejects an existing file with an unsupported extension" do + assert {:error, "Invalid file path or format."} = Helpers.read_and_validate_file("mix.exs") + end + + test "rejects a directory" do + assert {:error, "Invalid file path or format."} = Helpers.read_and_validate_file("lib") + end + + test "surfaces the posix reason when the path is a directory named like a file", %{tmp: tmp} do + path = Path.join(tmp, "iAmADirectory.js") + File.mkdir_p!(path) + + assert {:error, :eisdir} = Helpers.read_and_validate_file(path) + end + end + + describe "call_nif_fn/4" do + test "defaults to :content and passes the content straight through" do + result = + Helpers.call_nif_fn("some content", {:my_caller, 2}, fn content -> + {:ok, :ignored_atom, String.upcase(content)} + end) + + assert {:ok, :my_caller, "SOME CONTENT"} = result + end + + test ":path reads the file before invoking the processing function" do + result = + Helpers.call_nif_fn( + @valid_app_js, + {:my_caller, 2}, + fn content -> {:ok, :ignored_atom, String.length(content)} end, + :path + ) + + assert {:ok, :my_caller, length} = result + assert length > 0 + end + + test ":path returns an error tuple without calling the processing function" do + result = + Helpers.call_nif_fn( + "missing.js", + {:my_caller, 2}, + fn _content -> raise "processing function must not run" end, + :path + ) + + assert {:error, :my_caller, "Invalid file path or format."} = result + end + end + + describe "normalize_output/2" do + test "replaces the middle element with the caller function name" do + assert {:ok, :caller_name, "payload"} = + Helpers.normalize_output({:ok, :from_nif, "payload"}, {:caller_name, 1}) + + assert {:error, :caller_name, "reason"} = + Helpers.normalize_output({:error, :from_nif, "reason"}, {:caller_name, 1}) + end + end +end diff --git a/test/parsers/css/formatter_test.exs b/test/parsers/css/formatter_test.exs index ce15616..df45240 100644 --- a/test/parsers/css/formatter_test.exs +++ b/test/parsers/css/formatter_test.exs @@ -17,4 +17,70 @@ defmodule IgniterJSTest.Parsers.CSS.FormatterTest do {:ok, _, formatted} = assert Formatter.format("body { color: red; }") ^formatted = assert "body {\n color: red;\n}\n" end + + describe "invalid input" do + @invalid_css "body { color" + + test "format/2 reports a syntax error" do + assert {:error, :format, "Parsing failed due to syntax errors."} = + Formatter.format(@invalid_css, :content) + end + + test "is_formatted/2 reports false without raising" do + assert {:error, :is_formatted, false} = Formatter.is_formatted(@invalid_css, :content) + end + + test "is_formatted?/2 returns false" do + refute Formatter.is_formatted?(@invalid_css, :content) + end + end + + describe ":path mode" do + setup do + path = + Path.join(System.tmp_dir!(), "igniter_js_#{System.unique_integer([:positive])}.css") + + File.write!(path, "body{color:red;}") + on_exit(fn -> File.rm(path) end) + {:ok, path: path} + end + + test "rejects a .css file because the extension allow-list is js/ts only", %{path: path} do + assert {:error, :format, "Invalid file path or format."} = Formatter.format(path, :path) + end + + test "errors for a missing file" do + assert {:error, :format, "Invalid file path or format."} = + Formatter.format("missing.css", :path) + end + end + + describe "formatting properties" do + test "format/2 is idempotent" do + assert {:ok, :format, once} = Formatter.format("body{color:red;}", :content) + assert {:ok, :format, twice} = Formatter.format(once, :content) + assert once == twice + end + + test "uses two-space indentation" do + assert {:ok, :format, output} = Formatter.format("body{color:red;}", :content) + assert output == "body {\n color: red;\n}\n" + end + + test "its own output is considered formatted" do + assert {:ok, :format, output} = Formatter.format("body{color:red;}", :content) + assert Formatter.is_formatted?(output, :content) + end + + test "preserves comments" do + assert {:ok, :format, output} = + Formatter.format("/* keep me */\nbody{color:red;}", :content) + + assert output =~ "/* keep me */" + end + + test "empty content formats to empty output" do + assert {:ok, :format, ""} = Formatter.format("", :content) + end + end end diff --git a/test/parsers/javascript/formatter_test.exs b/test/parsers/javascript/formatter_test.exs index 9b62bee..9ca75d1 100644 --- a/test/parsers/javascript/formatter_test.exs +++ b/test/parsers/javascript/formatter_test.exs @@ -34,4 +34,61 @@ defmodule IgniterJSTest.Parsers.Javascript.FormatterTest do ^formatted = assert "function test() {\n // expose liveSocket on window for web console debug logs and latency simulation:\n console.log(\"hello world\");\n // expose liveSocket on window for web console debug logs and latency simulation:\n}\n" end + + describe "invalid input" do + @invalid_js "function {{{" + + test "format/2 reports a syntax error" do + assert {:error, :format, "Parsing failed due to syntax errors."} = + Formatter.format(@invalid_js, :content) + end + + test "is_formatted/2 reports false without raising" do + assert {:error, :is_formatted, false} = Formatter.is_formatted(@invalid_js, :content) + end + + test "is_formatted?/2 returns false" do + refute Formatter.is_formatted?(@invalid_js, :content) + end + end + + describe ":path mode" do + @valid_app_js "test/assets/validApp.js" + + test "format/2 formats a file from disk" do + assert {:ok, :format, output} = Formatter.format(@valid_app_js, :path) + assert output =~ "liveSocket" + end + + test "format/2 errors for a missing file" do + assert {:error, :format, "Invalid file path or format."} = + Formatter.format("test/assets/missing.js", :path) + end + + test "is_formatted?/2 errors safely for a missing file" do + refute Formatter.is_formatted?("test/assets/missing.js", :path) + end + end + + describe "formatting properties" do + test "format/2 is idempotent" do + assert {:ok, :format, once} = Formatter.format("const a=1;const b =2;", :content) + assert {:ok, :format, twice} = Formatter.format(once, :content) + assert once == twice + end + + test "its own output is considered formatted" do + assert {:ok, :format, output} = Formatter.format("const a=1;", :content) + assert Formatter.is_formatted?(output, :content) + end + + test "uses two-space indentation" do + assert {:ok, :format, output} = Formatter.format("function a(){return 1;}", :content) + assert output =~ "\n return 1;" + end + + test "empty content formats to empty output" do + assert {:ok, :format, ""} = Formatter.format("", :content) + end + end end diff --git a/test/parsers/javascript/parser_edge_cases_test.exs b/test/parsers/javascript/parser_edge_cases_test.exs new file mode 100644 index 0000000..9df6e8b --- /dev/null +++ b/test/parsers/javascript/parser_edge_cases_test.exs @@ -0,0 +1,325 @@ +# SPDX-FileCopyrightText: 2024 igniter_js contributors +# +# SPDX-License-Identifier: MIT + +defmodule IgniterJSTest.Parsers.Javascript.ParserEdgeCasesTest do + use ExUnit.Case + alias IgniterJs.Parsers.Javascript.Parser + + @valid_app_js "test/assets/validApp.js" + @missing_path "test/assets/thisFileDoesNotExist.js" + @unsupported_path "mix.exs" + + describe ":path mode with an unreadable file" do + defp path_cases do + [ + {:module_imported, fn path -> Parser.module_imported(path, "phoenix", :path) end}, + {:insert_imports, + fn path -> Parser.insert_imports(path, ~s|import a from "b";|, :path) end}, + {:remove_imports, fn path -> Parser.remove_imports(path, "phoenix", :path) end}, + {:exist_live_socket, fn path -> Parser.exist_live_socket(path, :path) end}, + {:exist_var, fn path -> Parser.exist_var(path, "Hooks", :path) end}, + {:extend_hook_object, fn path -> Parser.extend_hook_object(path, "Hook", :path) end}, + {:remove_objects_from_hooks, + fn path -> Parser.remove_objects_from_hooks(path, "Hook", :path) end}, + {:statistics, fn path -> Parser.statistics(path, :path) end}, + {:extend_var_object_by_object_names, + fn path -> Parser.extend_var_object_by_object_names(path, "C", "H", :path) end}, + {:ast_to_estree, fn path -> Parser.ast_to_estree(path, :path) end}, + {:insert_at_index, fn path -> Parser.insert_at_index(path, "let a = 1;", 0, :path) end}, + {:replace_at_index, fn path -> Parser.replace_at_index(path, "let a = 1;", 0, :path) end} + ] + end + + test "every entry point returns an error tuple for a missing file" do + for {fn_atom, call} <- path_cases() do + assert {:error, ^fn_atom, "Invalid file path or format."} = call.(@missing_path), + "#{fn_atom} did not return the expected error tuple for a missing file" + end + end + + test "every entry point returns an error tuple for an unsupported extension" do + for {fn_atom, call} <- path_cases() do + assert {:error, ^fn_atom, "Invalid file path or format."} = call.(@unsupported_path), + "#{fn_atom} did not return the expected error tuple for an unsupported extension" + end + end + + test "statistics/2 returns an error tuple instead of raising MatchError" do + assert {:error, :statistics, "Invalid file path or format."} = + Parser.statistics(@missing_path, :path) + end + + test "boolean helpers report false rather than raising for a missing file" do + refute Parser.module_imported?(@missing_path, "phoenix", :path) + refute Parser.exist_live_socket?(@missing_path, :path) + refute Parser.var_exists?(@missing_path, "Hooks", :path) + end + end + + describe "var_exists?/3" do + test "finds a let-declared variable in content" do + assert Parser.var_exists?("let Hooks = {};", "Hooks") + refute Parser.var_exists?("let Hooks = {};", "Missing") + end + + test "works against a file path" do + assert Parser.var_exists?(@valid_app_js, "Hooks", :path) + refute Parser.var_exists?(@valid_app_js, "NotThere", :path) + end + + test "agrees with the tuple-returning exist_var/3" do + assert {:ok, :exist_var, true} = Parser.exist_var("let Hooks = {};", "Hooks") + assert {:error, :exist_var, false} = Parser.exist_var("let Hooks = {};", "Missing") + end + end + + describe "empty and trivial input" do + test "statistics/2 reports all zeros for empty content" do + assert {:ok, :statistics, stats} = Parser.statistics("") + + assert %{functions: 0, classes: 0, debuggers: 0, imports: 0, trys: 0, throws: 0} = stats + end + + test "statistics/2 returns a plain map without the struct key" do + assert {:ok, :statistics, stats} = Parser.statistics("function a() {}") + refute Map.has_key?(stats, :__struct__) + assert stats.functions == 1 + end + + test "lookups on empty content report not-found rather than crashing" do + assert {:error, :exist_live_socket, false} = Parser.exist_live_socket("") + assert {:error, :module_imported, false} = Parser.module_imported("", "phoenix") + assert {:error, :exist_var, false} = Parser.exist_var("", "Hooks") + end + end + + describe "index boundaries" do + @code "let a = 1;\nlet b = 2;\n" + + test "insert_at_index/4 inserts at the very beginning" do + assert {:ok, :insert_at_index, output} = Parser.insert_at_index(@code, "let z = 0;", 0) + assert String.starts_with?(output, "let z = 0;") + end + + test "insert_at_index/4 rejects an out-of-bounds index" do + assert {:error, :insert_at_index, "Index out of bounds"} = + Parser.insert_at_index(@code, "let z = 0;", 99) + end + + test "replace_at_index/4 replaces the targeted statement" do + assert {:ok, :replace_at_index, output} = Parser.replace_at_index(@code, "let z = 0;", 0) + assert output =~ "let z = 0;" + refute output =~ "let a = 1;" + assert output =~ "let b = 2;" + end + + test "replace_at_index/4 rejects an out-of-bounds index" do + assert {:error, :replace_at_index, "Index out of bounds"} = + Parser.replace_at_index(@code, "let z = 0;", 99) + end + end + + describe "extend_var_object_by_object_names/4" do + test "errors when the target variable is absent" do + assert {:error, :extend_var_object_by_object_names, reason} = + Parser.extend_var_object_by_object_names("let x = 1;", "Missing", "Hook") + + assert is_binary(reason) + end + + test "de-duplicates repeated object names" do + code = "const Components = {};\n" + + assert {:ok, :extend_var_object_by_object_names, output} = + Parser.extend_var_object_by_object_names(code, "Components", [ + "A", + "B", + "A", + "B" + ]) + + assert length(Regex.scan(~r/\bA\b/, output)) == 1 + assert length(Regex.scan(~r/\bB\b/, output)) == 1 + end + end + + describe "ast_to_estree/2" do + test "captures both line and block comments with positions" do + assert {:ok, :ast_to_estree, ast} = + Parser.ast_to_estree("const a = 1; /* block */ // line", :content) + + assert Enum.map(ast["comments"], & &1["type"]) == ["Block", "Line"] + + for comment <- ast["comments"] do + assert is_integer(comment["start"]) + assert is_integer(comment["end"]) + assert comment["end"] > comment["start"] + end + end + + test "emits range alongside start/end on every node" do + assert {:ok, :ast_to_estree, ast} = Parser.ast_to_estree("const a = 1;", :content) + + program = ast["program"] + assert [start_pos, end_pos] = program["range"] + assert start_pos == program["start"] + assert end_pos == program["end"] + + node = hd(program["body"]) + assert node["type"] == "VariableDeclaration" + assert [_, _] = node["range"] + end + + test "reports syntax errors in the errors list instead of failing" do + assert {:ok, :ast_to_estree, ast} = Parser.ast_to_estree("const a = ;", :content) + + assert [error | _] = ast["errors"] + assert error["severity"] == "Error" + assert is_binary(error["message"]) + end + + test "returns an empty errors list for valid source" do + assert {:ok, :ast_to_estree, ast} = Parser.ast_to_estree("const a = 1;", :content) + assert ast["errors"] == [] + end + + test "parses a real file through :path" do + assert {:ok, :ast_to_estree, ast} = Parser.ast_to_estree(@valid_app_js, :path) + assert ast["program"]["type"] == "Program" + assert ast["errors"] == [] + end + end + + describe "import handling" do + test "insert_imports/3 does not duplicate an import that already exists" do + code = ~s|import { Socket } from "phoenix";\n| + + assert {:ok, :insert_imports, output} = + Parser.insert_imports(code, ~s|import { Socket } from "phoenix";|) + + assert length(Regex.scan(~r/from "phoenix"/, output)) == 1 + end + + test "insert_imports/3 takes several imports as one newline-separated string" do + code = "let a = 1;\n" + + assert {:ok, :insert_imports, output} = + Parser.insert_imports( + code, + ~s|import a from "mod-a";\nimport b from "mod-b";| + ) + + assert output =~ "mod-a" + assert output =~ "mod-b" + end + + test "insert_imports/3 rejects a list, which the NIF cannot decode" do + assert_raise ArgumentError, fn -> + Parser.insert_imports("let a = 1;\n", [~s|import a from "mod-a";|]) + end + end + + test "remove_imports/3 removes a matching full import statement" do + code = ~s|import a from "mod-a";\nimport b from "mod-b";\nlet x = 1;\n| + + assert {:ok, :remove_imports, output} = + Parser.remove_imports(code, ~s|import a from "mod-a";|) + + refute output =~ "mod-a" + assert output =~ "mod-b" + assert output =~ "let x = 1;" + end + + test "remove_imports/3 removes several imports given as one string" do + code = ~s|import a from "mod-a";\nimport b from "mod-b";\nlet x = 1;\n| + + assert {:ok, :remove_imports, output} = + Parser.remove_imports( + code, + ~s|import a from "mod-a";\nimport b from "mod-b";| + ) + + refute output =~ "mod-a" + refute output =~ "mod-b" + assert output =~ "let x = 1;" + end + + test "remove_imports/3 removes by bare module specifier" do + code = ~s|import a from "mod-a";\nimport b from "mod-b";\n| + + assert {:ok, :remove_imports, output} = Parser.remove_imports(code, "mod-a") + + refute output =~ "mod-a" + assert output =~ "mod-b" + end + + test "remove_imports/3 removes by a path-style specifier" do + code = ~s|import topbar from "../vendor/topbar";\nlet Hooks = {};\n| + + assert {:ok, :remove_imports, output} = Parser.remove_imports(code, "../vendor/topbar") + + refute output =~ "topbar" + assert output =~ "let Hooks" + end + + test "remove_imports/3 matches the module source, not the local binding" do + code = ~s|import topbar from "../vendor/topbar";\n| + + assert {:ok, :remove_imports, output} = Parser.remove_imports(code, "topbar") + + assert output =~ "../vendor/topbar" + end + + test "remove_imports/3 accepts several bare specifiers, one per line" do + code = ~s|import a from "mod-a";\nimport b from "mod-b";\nlet x = 1;\n| + + assert {:ok, :remove_imports, output} = Parser.remove_imports(code, "mod-a\nmod-b") + + refute output =~ "mod-a" + refute output =~ "mod-b" + assert output =~ "let x = 1;" + end + + test "remove_imports/3 leaves a multi-line import statement's inner lines alone" do + code = ~s|import x from "foo";\nimport { foo } from "mod-a";\n| + argument = ~s|import {\n foo\n} from "mod-a";| + + assert {:ok, :remove_imports, output} = Parser.remove_imports(code, argument) + + assert output =~ ~s|import x from "foo";| + refute output =~ "mod-a" + end + + test "remove_imports/3 is a no-op for an unmatched module" do + code = ~s|import a from "mod-a";\n| + + assert {:ok, :remove_imports, output} = Parser.remove_imports(code, "not-imported") + + assert output =~ "mod-a" + end + end + + describe "list arguments" do + test "extend_hook_object/3 accepts a list and de-duplicates" do + code = ~s|let liveSocket = new LiveSocket("/live", Socket, { hooks: {} });\n| + + assert {:ok, :extend_hook_object, output} = + Parser.extend_hook_object(code, ["A", "B", "A"]) + + assert length(Regex.scan(~r/\bA\b/, output)) == 1 + assert length(Regex.scan(~r/\bB\b/, output)) == 1 + end + + test "remove_objects_from_hooks/3 accepts a list" do + code = ~s|let liveSocket = new LiveSocket("/live", Socket, { hooks: { A, B, C } });\n| + + assert {:ok, :remove_objects_from_hooks, output} = + Parser.remove_objects_from_hooks(code, ["A", "C"]) + + refute output =~ "A" + assert output =~ "B" + refute output =~ "C" + end + end +end diff --git a/test/parsers/javascript/parser_test.exs b/test/parsers/javascript/parser_test.exs index 2b3e558..be14e9a 100644 --- a/test/parsers/javascript/parser_test.exs +++ b/test/parsers/javascript/parser_test.exs @@ -147,8 +147,7 @@ defmodule IgniterJSTest.Parsers.Javascript.ParserTest do ^none_imported_module_output = assert outptu - remove_a_module_output = - "import { foo } from \"module-name\";\nimport bar from \"another-module\";\nlet Hooks = {};\n" + remove_a_module_output = "import bar from \"another-module\";\nlet Hooks = {};\n" {:ok, :remove_imports, outptu} = Parser.remove_imports(File.read!(@invalid_app_with_removed_import), "module-name") @@ -755,6 +754,39 @@ defmodule IgniterJSTest.Parsers.Javascript.ParserTest do assert length(Regex.scan(~r/\bNewHook\b/, output)) == 1 end + test "Unparseable arguments return an error tuple instead of panicking the NIF" do + js_code = """ + import { Socket } from "phoenix"; + import topbar from "../vendor/topbar"; + let Hooks = {}; + """ + + assert {:error, :insert_imports, _reason} = + Parser.insert_imports(js_code, "../vendor/topbar", :content) + + assert {:error, :module_imported, false} = + Parser.module_imported(js_code, "../vendor/topbar", :content) + + refute Parser.module_imported?(js_code, "../vendor/topbar", :content) + + assert {:ok, :remove_imports, output} = + Parser.remove_imports(js_code, ~s|import topbar from "../vendor/topbar";|, :content) + + refute String.contains?(output, "topbar") + end + + test "Unparseable source returns an error tuple instead of panicking the NIF" do + broken = "let x = ;;; import * from;" + + assert {:error, :remove_imports, _reason} = + Parser.remove_imports(broken, "topbar", :content) + + assert {:error, :exist_var, _reason} = Parser.exist_var(broken, "x", :content) + + assert {:error, :remove_objects_from_hooks, _reason} = + Parser.remove_objects_from_hooks(broken, "SomeHook", :content) + end + defp string_counter(string, pattern) do Regex.scan(Regex.compile!(pattern), string) |> length()