diff --git a/.gitignore b/.gitignore index 9ed78731..a38d8f33 100644 --- a/.gitignore +++ b/.gitignore @@ -18,12 +18,14 @@ bar_debug_launcher repos.local.conf .env +# Build artifacts +bar-lua-codemod/target/ + # Runtime / editor state tasks/ .devtools/ .backups/ *.log -bar-lua-codemod/target/ .claude/settings.local.json .claude/projects/ .claude/todos/ diff --git a/bar-lua-codemod/Cargo.lock b/bar-lua-codemod/Cargo.lock new file mode 100644 index 00000000..dab1015f --- /dev/null +++ b/bar-lua-codemod/Cargo.lock @@ -0,0 +1,701 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "bar-lua-codemod" +version = "0.1.0" +dependencies = [ + "clap", + "emmylua_parser", + "glob", +] + +[[package]] +name = "base62" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bstr" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "emmylua_parser" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0103cc231288ddc9391785db73be15d5928951e54cfe17c73af8c4d371678db" +dependencies = [ + "rowan", + "rust-i18n", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ignore" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "normpath" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash", + "text-size", +] + +[[package]] +name = "rust-i18n" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yaml", + "syn", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "toml", + "triomphe", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "triomphe" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +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.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/bar-lua-codemod/Cargo.toml b/bar-lua-codemod/Cargo.toml new file mode 100644 index 00000000..44f4d3f3 --- /dev/null +++ b/bar-lua-codemod/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bar-lua-codemod" +version = "0.1.0" +edition = "2021" + +[dependencies] +emmylua_parser = "0.28" +glob = "0.3" +clap = { version = "4", features = ["derive"] } diff --git a/bar-lua-codemod/src/bracket_to_dot.rs b/bar-lua-codemod/src/bracket_to_dot.rs new file mode 100644 index 00000000..ef93bb1d --- /dev/null +++ b/bar-lua-codemod/src/bracket_to_dot.rs @@ -0,0 +1,231 @@ +use crate::cst::{bracket_span, bracket_string_key, quoted_content}; +use crate::edit::{self, Edit}; +use emmylua_parser::{LuaAstNode, LuaIndexExpr, LuaSyntaxTree, LuaTableField}; + +const LUA_RESERVED: &[&str] = &[ + "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", + "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while", +]; + +fn is_convertible_identifier(s: &str) -> bool { + let mut chars = s.chars(); + match chars.next() { + Some(c) if c.is_ascii_alphabetic() || c == '_' => {} + _ => return false, + } + chars.all(|c| c.is_ascii_alphanumeric() || c == '_') && !LUA_RESERVED.contains(&s) +} + +pub struct BracketToDot { + pub index_conversions: usize, + pub field_conversions: usize, + pub skipped_reserved: usize, +} + +impl BracketToDot { + pub fn new() -> Self { + Self { + index_conversions: 0, + field_conversions: 0, + skipped_reserved: 0, + } + } + + pub fn rewrite(&mut self, source: &str, tree: &LuaSyntaxTree) -> String { + let mut edits: Vec = Vec::new(); + for node in tree.get_chunk_node().syntax().descendants() { + if let Some(index) = LuaIndexExpr::cast(node.clone()) { + self.index_expr(source, &index, &mut edits); + } else if let Some(field) = LuaTableField::cast(node) { + self.table_field(&field, &mut edits); + } + } + edit::apply(source, edits) + } + + /// x["y"] -> x.y; a space is injected when `]` abuts a word character + /// (]keyword is fine, .identifierkeyword merges). + fn index_expr(&mut self, source: &str, index: &LuaIndexExpr, edits: &mut Vec) { + let Some(token) = bracket_string_key(index.syntax()) else { + return; + }; + let Some(name) = quoted_content(&token) else { + return; + }; + if is_convertible_identifier(&name) { + let Some((start, end)) = bracket_span(index.syntax()) else { + return; + }; + self.index_conversions += 1; + let mut text = format!(".{name}"); + let next_is_word = source + .as_bytes() + .get(end) + .map(|&b| b.is_ascii_alphanumeric() || b == b'_') + .unwrap_or(false); + if next_is_word { + text.push(' '); + } + edits.push(Edit { start, end, text }); + } else if LUA_RESERVED.contains(&name.as_str()) { + self.skipped_reserved += 1; + } + } + + /// ["y"] = v -> y = v (table constructor fields). + fn table_field(&mut self, field: &LuaTableField, edits: &mut Vec) { + if !field.is_assign_field() { + return; + } + let Some(token) = bracket_string_key(field.syntax()) else { + return; + }; + let Some(name) = quoted_content(&token) else { + return; + }; + if is_convertible_identifier(&name) { + let Some((start, end)) = bracket_span(field.syntax()) else { + return; + }; + self.field_conversions += 1; + edits.push(Edit { start, end, text: name }); + } else if LUA_RESERVED.contains(&name.as_str()) { + self.skipped_reserved += 1; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cst::parse; + + fn transform(input: &str) -> (String, usize, usize) { + let tree = parse(input).expect("parse failed"); + let mut visitor = BracketToDot::new(); + let out = visitor.rewrite(input, &tree); + (out, visitor.index_conversions, visitor.field_conversions) + } + + #[test] + fn index_simple() { + let (out, idx, fld) = transform(r#"local x = t["foo"]"#); + assert_eq!(out, "local x = t.foo"); + assert_eq!(idx, 1); + assert_eq!(fld, 0); + } + + #[test] + fn index_single_quotes() { + let (out, idx, _) = transform("local x = t['bar']"); + assert_eq!(out, "local x = t.bar"); + assert_eq!(idx, 1); + } + + #[test] + fn index_chained() { + let (out, idx, _) = transform(r#"local x = t["a"]["b"]"#); + assert_eq!(out, "local x = t.a.b"); + assert_eq!(idx, 2); + } + + #[test] + fn index_reserved_word_skipped() { + let (out, _, _) = transform(r#"local x = t["end"]"#); + assert_eq!(out, r#"local x = t["end"]"#); + } + + #[test] + fn index_numeric_key_skipped() { + let (out, idx, _) = transform(r#"local x = t["123"]"#); + assert_eq!(out, r#"local x = t["123"]"#); + assert_eq!(idx, 0); + } + + #[test] + fn index_special_chars_skipped() { + let (out, idx, _) = transform(r#"local x = t["foo-bar"]"#); + assert_eq!(out, r#"local x = t["foo-bar"]"#); + assert_eq!(idx, 0); + } + + #[test] + fn field_simple() { + let (out, idx, fld) = transform(r#"local t = { ["foo"] = 1 }"#); + assert_eq!(out, "local t = { foo = 1 }"); + assert_eq!(idx, 0); + assert_eq!(fld, 1); + } + + #[test] + fn field_reserved_word_skipped() { + let (out, _, fld) = transform(r#"local t = { ["end"] = 1 }"#); + assert_eq!(out, r#"local t = { ["end"] = 1 }"#); + assert_eq!(fld, 0); + } + + #[test] + fn mixed_conversions() { + let (out, idx, fld) = transform(r#"t["x"] = { ["y"] = 1 }"#); + assert_eq!(out, "t.x = { y = 1 }"); + assert_eq!(idx, 1); + assert_eq!(fld, 1); + } + + #[test] + fn underscore_identifier() { + let (out, idx, _) = transform(r#"local x = t["_private"]"#); + assert_eq!(out, "local x = t._private"); + assert_eq!(idx, 1); + } + + #[test] + fn no_changes() { + let (out, idx, fld) = transform("local x = t[42]"); + assert_eq!(out, "local x = t[42]"); + assert_eq!(idx, 0); + assert_eq!(fld, 0); + } + + #[test] + fn bracket_then_dot_access() { + let (out, idx, _) = transform(r#"local x = cmd[1]["options"].ctrl"#); + assert_eq!(out, "local x = cmd[1].options.ctrl"); + assert_eq!(idx, 1); + } + + #[test] + fn bracket_to_dot_then_dot_access() { + let (out, idx, _) = transform(r#"local x = WeaponDefNames["lightning_chain"].id"#); + assert_eq!(out, "local x = WeaponDefNames.lightning_chain.id"); + assert_eq!(idx, 1); + } + + #[test] + fn no_merge_with_following_keyword() { + let (out, idx, _) = transform("if force and WG['guishader']then end"); + assert!(out.contains("WG.guishader then"), "got: {out}"); + assert_eq!(idx, 1); + } + + #[test] + fn no_merge_with_following_identifier() { + let (out, idx, _) = transform("local x = t['key']or false"); + assert!(out.contains("t.key or"), "got: {out}"); + assert_eq!(idx, 1); + } + + #[test] + fn escape_in_string_skipped() { + let (out, idx, _) = transform(r#"local x = t["\097bc"]"#); + assert_eq!(out, r#"local x = t["\097bc"]"#); + assert_eq!(idx, 0); + } + + #[test] + fn inner_bracket_trivia_dropped() { + let (out, idx, _) = transform(r#"local x = t[ "foo" ]"#); + assert_eq!(out, "local x = t.foo"); + assert_eq!(idx, 1); + } +} diff --git a/bar-lua-codemod/src/cst.rs b/bar-lua-codemod/src/cst.rs new file mode 100644 index 00000000..67bd4887 --- /dev/null +++ b/bar-lua-codemod/src/cst.rs @@ -0,0 +1,116 @@ +use emmylua_parser::{ + LuaAstNode, LuaAstToken, LuaLanguageLevel, LuaLiteralExpr, LuaLiteralToken, LuaParseErrorKind, + LuaParser, LuaStringToken, LuaSyntaxKind, LuaSyntaxNode, LuaSyntaxToken, LuaSyntaxTree, + LuaTokenKind, ParserConfig, +}; +use std::collections::HashMap; + +/// Lua 5.1 (BAR's runtime level), doc-comment parsing off: comments are +/// trivia, as they were under full_moon. +pub fn parse(code: &str) -> Result { + let config = ParserConfig::new( + LuaLanguageLevel::Lua51, + None, + HashMap::new(), + Default::default(), + false, + ); + let tree = LuaParser::parse(code, config); + if tree.has_syntax_errors() { + return Err(tree + .get_errors() + .iter() + .filter(|e| e.kind == LuaParseErrorKind::SyntaxError) + .map(|e| e.message.clone()) + .collect::>() + .join("; ")); + } + Ok(tree) +} + +/// Byte range from `[` through `]` of an index expr or table field node. +pub fn bracket_span(node: &LuaSyntaxNode) -> Option<(usize, usize)> { + let mut start = None; + for child in node.children_with_tokens() { + let Some(token) = child.as_token() else { + continue; + }; + if token.kind() == LuaTokenKind::TkLeftBracket.into() && start.is_none() { + start = Some(usize::from(token.text_range().start())); + } else if token.kind() == LuaTokenKind::TkRightBracket.into() { + return Some((start?, usize::from(token.text_range().end()))); + } + } + None +} + +fn is_trivia(token: &LuaSyntaxToken) -> bool { + token.kind() == LuaTokenKind::TkWhitespace.into() + || token.kind() == LuaTokenKind::TkEndOfLine.into() + || token.kind() == LuaTokenKind::TkShortComment.into() + || token.kind() == LuaTokenKind::TkLongComment.into() +} + +/// The sole string-literal key of `[...]` in an index expr or table field, +/// tolerating trivia inside the brackets (which the upstream get_index_key +/// does not). None when the bracketed expression is anything else. +pub fn bracket_string_key(node: &LuaSyntaxNode) -> Option { + let mut in_brackets = false; + let mut key: Option = None; + for child in node.children_with_tokens() { + if !in_brackets { + if child.as_token().map(|t| t.kind()) == Some(LuaTokenKind::TkLeftBracket.into()) { + in_brackets = true; + } + continue; + } + if let Some(token) = child.as_token() { + if token.kind() == LuaTokenKind::TkRightBracket.into() { + return key; + } + if !is_trivia(token) { + return None; + } + } else if let Some(inner) = child.into_node() { + if key.is_some() { + return None; + } + let literal = LuaLiteralExpr::cast(inner)?; + match literal.get_literal()? { + LuaLiteralToken::String(token) => key = Some(token), + _ => return None, + } + } + } + None +} + +/// Raw content of a single- or double-quoted string token; None for long +/// strings. No unescaping — parity with the raw-slice rule the conversions +/// were generated under. +pub fn quoted_content(token: &LuaStringToken) -> Option { + let s = token.get_text(); + if s.len() >= 2 + && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\''))) + { + return Some(s[1..s.len() - 1].to_string()); + } + None +} + +/// True when the index chain is the name of `function a.b.c() end` — +/// a position full_moon's Var/FunctionCall visitors never rewrote. +pub fn is_func_stat_name(node: &LuaSyntaxNode) -> bool { + let mut cur = node.clone(); + loop { + let Some(parent) = cur.parent() else { + return false; + }; + let kind: LuaSyntaxKind = parent.kind().into(); + if kind == LuaSyntaxKind::IndexExpr { + cur = parent; + continue; + } + return kind == LuaSyntaxKind::FuncStat; + } +} diff --git a/bar-lua-codemod/src/detach_bar_modules.rs b/bar-lua-codemod/src/detach_bar_modules.rs new file mode 100644 index 00000000..1b216cb3 --- /dev/null +++ b/bar-lua-codemod/src/detach_bar_modules.rs @@ -0,0 +1,178 @@ +use crate::cst::is_func_stat_name; +use crate::edit::{self, Edit}; +use emmylua_parser::{ + LuaAstNode, LuaAstToken, LuaExpr, LuaIndexExpr, LuaIndexKey, LuaSyntaxTree, +}; +use std::collections::HashSet; + +pub struct DetachBarModules { + modules: HashSet, + pub conversions: usize, +} + +impl DetachBarModules { + pub fn new(modules: &[&str]) -> Self { + Self { + modules: modules.iter().map(|s| s.to_string()).collect(), + conversions: 0, + } + } + + /// Match `Spring.Module` or `_G.Spring.Module` and rename the Spring + /// segment to `BAR`, keeping the module name and everything after it + /// (`Spring.I18N.t()` -> `BAR.I18N.t()`). + pub fn rewrite(&mut self, source: &str, tree: &LuaSyntaxTree) -> String { + let mut edits: Vec = Vec::new(); + for node in tree.get_chunk_node().syntax().descendants() { + let Some(index) = LuaIndexExpr::cast(node) else { + continue; + }; + if is_func_stat_name(index.syntax()) { + continue; + } + let Some(LuaIndexKey::Name(module)) = index.get_index_key() else { + continue; + }; + if !self.modules.contains(module.get_name_text()) { + continue; + } + let spring_range = match index.get_prefix_expr() { + Some(LuaExpr::NameExpr(prefix)) + if prefix.get_name_text().as_deref() == Some("Spring") => + { + prefix.syntax().text_range() + } + Some(LuaExpr::IndexExpr(inner)) => { + let Some(LuaExpr::NameExpr(base)) = inner.get_prefix_expr() else { + continue; + }; + if base.get_name_text().as_deref() != Some("_G") { + continue; + } + let Some(LuaIndexKey::Name(spring)) = inner.get_index_key() else { + continue; + }; + if spring.get_name_text() != "Spring" { + continue; + } + spring.get_range() + } + _ => continue, + }; + self.conversions += 1; + edits.push(Edit { + start: usize::from(spring_range.start()), + end: usize::from(spring_range.end()), + text: "BAR".to_string(), + }); + } + edit::apply(source, edits) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cst::parse; + + const MODULES: &[&str] = &["I18N", "Utilities", "Debug", "Lava"]; + + fn transform(input: &str) -> (String, usize) { + let tree = parse(input).expect("parse failed"); + let mut visitor = DetachBarModules::new(MODULES); + let out = visitor.rewrite(input, &tree); + (out, visitor.conversions) + } + + #[test] + fn simple_call() { + let (out, n) = transform("Spring.I18N.translate(key)"); + assert_eq!(out, "BAR.I18N.translate(key)"); + assert_eq!(n, 1); + } + + #[test] + fn method_access() { + let (out, n) = transform("local x = Spring.Utilities.Round(1.5)"); + assert_eq!(out, "local x = BAR.Utilities.Round(1.5)"); + assert_eq!(n, 1); + } + + #[test] + fn var_reference() { + let (out, n) = transform("local u = Spring.Utilities"); + assert_eq!(out, "local u = BAR.Utilities"); + assert_eq!(n, 1); + } + + #[test] + fn non_module_unchanged() { + let (out, n) = transform("Spring.GetGameFrame()"); + assert_eq!(out, "Spring.GetGameFrame()"); + assert_eq!(n, 0); + } + + #[test] + fn non_spring_unchanged() { + let (out, n) = transform("Other.I18N.translate(key)"); + assert_eq!(out, "Other.I18N.translate(key)"); + assert_eq!(n, 0); + } + + #[test] + fn preserves_trivia() { + let (out, n) = transform(" Spring.Debug.log(msg) -- log it"); + assert_eq!(out, " BAR.Debug.log(msg) -- log it"); + assert_eq!(n, 1); + } + + #[test] + fn assignment_declaration() { + let (out, n) = transform("Spring.I18N = Spring.I18N or VFS.Include('i18n.lua')"); + assert_eq!(out, "BAR.I18N = BAR.I18N or VFS.Include('i18n.lua')"); + assert_eq!(n, 2); + } + + #[test] + fn multiple_in_one_file() { + let (out, n) = transform("Spring.I18N.t('x')\nSpring.Lava.isActive()"); + assert!(out.contains("BAR.I18N.t('x')")); + assert!(out.contains("BAR.Lava.isActive()")); + assert_eq!(n, 2); + } + + #[test] + fn g_spring_module_assignment() { + let (out, n) = transform("_G.Spring.Utilities = _G.Spring.Utilities or {}"); + assert_eq!(out, "_G.BAR.Utilities = _G.BAR.Utilities or {}"); + assert_eq!(n, 2); + } + + #[test] + fn g_spring_module_call() { + let (out, n) = transform("_G.Spring.I18N('key')"); + assert_eq!(out, "_G.BAR.I18N('key')"); + assert_eq!(n, 1); + } + + #[test] + fn g_spring_non_module_unchanged() { + let (out, n) = transform("_G.Spring.GetGameFrame()"); + assert_eq!(out, "_G.Spring.GetGameFrame()"); + assert_eq!(n, 0); + } + + #[test] + fn g_spring_deep_access() { + let (out, n) = transform("_G.Spring.Utilities.Gametype.IsFFA()"); + assert_eq!(out, "_G.BAR.Utilities.Gametype.IsFFA()"); + assert_eq!(n, 1); + } + + #[test] + fn function_definition_name_unchanged() { + let (out, n) = transform("function Spring.Utilities.Round(x) return x end"); + assert_eq!(out, "function Spring.Utilities.Round(x) return x end"); + assert_eq!(n, 0); + } +} diff --git a/bar-lua-codemod/src/edit.rs b/bar-lua-codemod/src/edit.rs new file mode 100644 index 00000000..06cc6942 --- /dev/null +++ b/bar-lua-codemod/src/edit.rs @@ -0,0 +1,21 @@ +/// A byte-range replacement against the original source. Transforms collect +/// edits over the CST walk; non-overlapping by construction (each edit spans +/// tokens of a distinct node). +pub struct Edit { + pub start: usize, + pub end: usize, + pub text: String, +} + +pub fn apply(source: &str, mut edits: Vec) -> String { + edits.sort_by_key(|e| e.start); + let mut out = String::with_capacity(source.len()); + let mut pos = 0; + for e in edits { + out.push_str(&source[pos..e.start]); + out.push_str(&e.text); + pos = e.end; + } + out.push_str(&source[pos..]); + out +} diff --git a/bar-lua-codemod/src/main.rs b/bar-lua-codemod/src/main.rs new file mode 100644 index 00000000..ff91df02 --- /dev/null +++ b/bar-lua-codemod/src/main.rs @@ -0,0 +1,380 @@ +use clap::{Parser, Subcommand}; +use std::path::PathBuf; +use std::{fs, process}; + +mod bracket_to_dot; +mod cst; +mod detach_bar_modules; +mod edit; +mod rename_aliases; + +#[derive(Parser)] +#[command(name = "bar-lua-codemod")] +#[command(about = "AST-based Lua codemod tool for Beyond All Reason")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Convert bracket string access to dot notation (x["y"] -> x.y, ["y"] = -> y =) + BracketToDot { + /// Root directory to process + #[arg(long, default_value = ".")] + path: PathBuf, + + /// Directories to exclude (relative to path, may be repeated) + #[arg(long)] + exclude: Vec, + + /// Report changes without writing files + #[arg(long)] + dry_run: bool, + }, + + /// Rename deprecated Spring method aliases to canonical names + RenameAliases { + /// Root directory to process + #[arg(long, default_value = ".")] + path: PathBuf, + + /// Directories to exclude (relative to path, may be repeated) + #[arg(long)] + exclude: Vec, + + /// Report changes without writing files + #[arg(long)] + dry_run: bool, + }, + + /// Detach BAR modules from the Spring table into the BAR namespace (Spring.I18N -> BAR.I18N, etc.) + DetachBarModules { + /// Root directory to process + #[arg(long, default_value = ".")] + path: PathBuf, + + /// Directories to exclude (relative to path, may be repeated) + #[arg(long)] + exclude: Vec, + + /// Report changes without writing files + #[arg(long)] + dry_run: bool, + }, +} + +fn collect_lua_files(root: &PathBuf, excludes: &[String]) -> Vec { + let pattern = format!("{}/**/*.lua", root.display()); + let mut files = Vec::new(); + for entry in glob::glob(&pattern).expect("invalid glob pattern") { + if let Ok(path) = entry { + let rel = path.strip_prefix(root).unwrap_or(&path); + let excluded = excludes + .iter() + .any(|ex| rel.starts_with(ex)); + if !excluded { + files.push(path); + } + } + } + files.sort(); + files +} + +fn format_num(n: usize) -> String { + let s = n.to_string(); + let bytes = s.as_bytes(); + let len = bytes.len(); + let mut result = String::new(); + for (i, &b) in bytes.iter().enumerate() { + if i > 0 && (len - i) % 3 == 0 { + result.push(','); + } + result.push(b as char); + } + result +} + +fn run_bracket_to_dot(root: &PathBuf, excludes: &[String], dry_run: bool) { + let files = collect_lua_files(root, excludes); + let total_files = files.len(); + + if total_files == 0 { + eprintln!("No .lua files found under {}", root.display()); + process::exit(1); + } + + let mut files_changed: usize = 0; + let mut total_index: usize = 0; + let mut total_field: usize = 0; + let mut total_skipped: usize = 0; + let mut errors: usize = 0; + let mut per_file: Vec<(PathBuf, usize, usize)> = Vec::new(); + + for file_path in &files { + let code = match fs::read_to_string(file_path) { + Ok(c) => c, + Err(e) => { + eprintln!(" error reading {}: {}", file_path.display(), e); + errors += 1; + continue; + } + }; + + let tree = match cst::parse(&code) { + Ok(t) => t, + Err(e) => { + eprintln!(" parse error in {}: {}", file_path.display(), e); + errors += 1; + continue; + } + }; + + let mut visitor = bracket_to_dot::BracketToDot::new(); + let new_code = visitor.rewrite(&code, &tree); + + if visitor.index_conversions > 0 || visitor.field_conversions > 0 { + if !dry_run { + if let Err(e) = fs::write(file_path, new_code) { + eprintln!(" error writing {}: {}", file_path.display(), e); + errors += 1; + continue; + } + } + files_changed += 1; + total_index += visitor.index_conversions; + total_field += visitor.field_conversions; + total_skipped += visitor.skipped_reserved; + per_file.push(( + file_path.clone(), + visitor.index_conversions, + visitor.field_conversions, + )); + } + } + + let total_conversions = total_index + total_field; + + if dry_run { + println!("bar-lua-codemod bracket-to-dot (DRY RUN):"); + } else { + println!("bar-lua-codemod bracket-to-dot results:"); + } + println!(" Files scanned: {:>30}", format_num(total_files)); + println!(" Files changed: {:>30}", format_num(files_changed)); + println!( + " Index conversions (x[\"y\"] -> x.y): {:>8}", + format_num(total_index) + ); + println!( + " Field conversions ([\"y\"] = -> y =): {:>8}", + format_num(total_field) + ); + println!( + " Total conversions: {:>8}", + format_num(total_conversions) + ); + println!( + " Skipped (reserved words): {:>8}", + format_num(total_skipped) + ); + println!( + " Errors (parse failures): {:>8}", + format_num(errors) + ); + + if !per_file.is_empty() { + per_file.sort_by(|a, b| (b.1 + b.2).cmp(&(a.1 + a.2))); + println!(); + println!("Top files by conversion count:"); + for (path, idx, fld) in per_file.iter().take(20) { + let rel = path.strip_prefix(root).unwrap_or(path); + println!(" {:<60} {:>5}", rel.display(), idx + fld); + } + } + + if errors > 0 { + process::exit(1); + } +} + +const BAR_ALIASES: &[(&str, &str)] = &[ + ("GetMyTeamID", "GetLocalTeamID"), + ("GetMyAllyTeamID", "GetLocalAllyTeamID"), + ("GetMyPlayerID", "GetLocalPlayerID"), +]; + +fn run_rename_aliases(root: &PathBuf, excludes: &[String], dry_run: bool) { + let files = collect_lua_files(root, excludes); + let total_files = files.len(); + + if total_files == 0 { + eprintln!("No .lua files found under {}", root.display()); + process::exit(1); + } + + let mut files_changed: usize = 0; + let mut total_conversions: usize = 0; + let mut errors: usize = 0; + let mut per_file: Vec<(PathBuf, usize)> = Vec::new(); + + for file_path in &files { + let code = match fs::read_to_string(file_path) { + Ok(c) => c, + Err(e) => { + eprintln!(" error reading {}: {}", file_path.display(), e); + errors += 1; + continue; + } + }; + + let tree = match cst::parse(&code) { + Ok(t) => t, + Err(e) => { + eprintln!(" parse error in {}: {}", file_path.display(), e); + errors += 1; + continue; + } + }; + + let mut visitor = rename_aliases::RenameAliases::new(BAR_ALIASES); + let new_code = visitor.rewrite(&code, &tree); + + if visitor.conversions > 0 { + if !dry_run { + if let Err(e) = fs::write(file_path, new_code) { + eprintln!(" error writing {}: {}", file_path.display(), e); + errors += 1; + continue; + } + } + files_changed += 1; + total_conversions += visitor.conversions; + per_file.push((file_path.clone(), visitor.conversions)); + } + } + + if dry_run { + println!("bar-lua-codemod rename-aliases (DRY RUN):"); + } else { + println!("bar-lua-codemod rename-aliases results:"); + } + println!(" Files scanned: {:>7}", format_num(total_files)); + println!(" Files changed: {:>7}", format_num(files_changed)); + println!(" Conversions: {:>7}", format_num(total_conversions)); + println!(" Errors: {:>7}", format_num(errors)); + + if !per_file.is_empty() { + per_file.sort_by(|a, b| b.1.cmp(&a.1)); + println!(); + println!("Top files by conversion count:"); + for (path, count) in per_file.iter().take(20) { + let rel = path.strip_prefix(root).unwrap_or(path); + println!(" {:<60} {:>5}", rel.display(), count); + } + } + + if errors > 0 { + process::exit(1); + } +} + +const BAR_MODULES: &[&str] = &["I18N", "Utilities", "Debug", "Lava", "GetModOptionsCopy"]; + +fn run_detach_bar_modules(root: &PathBuf, excludes: &[String], dry_run: bool) { + let files = collect_lua_files(root, excludes); + let total_files = files.len(); + + if total_files == 0 { + eprintln!("No .lua files found under {}", root.display()); + process::exit(1); + } + + let mut files_changed: usize = 0; + let mut total_conversions: usize = 0; + let mut errors: usize = 0; + let mut per_file: Vec<(PathBuf, usize)> = Vec::new(); + + for file_path in &files { + let code = match fs::read_to_string(file_path) { + Ok(c) => c, + Err(e) => { + eprintln!(" error reading {}: {}", file_path.display(), e); + errors += 1; + continue; + } + }; + + let tree = match cst::parse(&code) { + Ok(t) => t, + Err(e) => { + eprintln!(" parse error in {}: {}", file_path.display(), e); + errors += 1; + continue; + } + }; + + let mut visitor = detach_bar_modules::DetachBarModules::new(BAR_MODULES); + let new_code = visitor.rewrite(&code, &tree); + + if visitor.conversions > 0 { + if !dry_run { + if let Err(e) = fs::write(file_path, new_code) { + eprintln!(" error writing {}: {}", file_path.display(), e); + errors += 1; + continue; + } + } + files_changed += 1; + total_conversions += visitor.conversions; + per_file.push((file_path.clone(), visitor.conversions)); + } + } + + if dry_run { + println!("bar-lua-codemod detach-bar-modules (DRY RUN):"); + } else { + println!("bar-lua-codemod detach-bar-modules results:"); + } + println!(" Modules detached: {:>7}", BAR_MODULES.join(", ")); + println!(" Files scanned: {:>7}", format_num(total_files)); + println!(" Files changed: {:>7}", format_num(files_changed)); + println!(" Conversions: {:>7}", format_num(total_conversions)); + println!(" Errors: {:>7}", format_num(errors)); + + if !per_file.is_empty() { + per_file.sort_by(|a, b| b.1.cmp(&a.1)); + println!(); + println!("Top files by conversion count:"); + for (path, count) in per_file.iter().take(20) { + let rel = path.strip_prefix(root).unwrap_or(path); + println!(" {:<60} {:>5}", rel.display(), count); + } + } + + if errors > 0 { + process::exit(1); + } +} + +fn main() { + let cli = Cli::parse(); + match cli.command { + Commands::BracketToDot { + path, + exclude, + dry_run, + } => run_bracket_to_dot(&path, &exclude, dry_run), + Commands::RenameAliases { + path, + exclude, + dry_run, + } => run_rename_aliases(&path, &exclude, dry_run), + Commands::DetachBarModules { + path, + exclude, + dry_run, + } => run_detach_bar_modules(&path, &exclude, dry_run), + } +} diff --git a/bar-lua-codemod/src/rename_aliases.rs b/bar-lua-codemod/src/rename_aliases.rs new file mode 100644 index 00000000..323c603d --- /dev/null +++ b/bar-lua-codemod/src/rename_aliases.rs @@ -0,0 +1,127 @@ +use crate::cst::is_func_stat_name; +use crate::edit::{self, Edit}; +use emmylua_parser::{ + LuaAstNode, LuaAstToken, LuaExpr, LuaIndexExpr, LuaIndexKey, LuaSyntaxTree, +}; +use std::collections::HashMap; + +pub struct RenameAliases { + aliases: HashMap, + pub conversions: usize, +} + +impl RenameAliases { + pub fn new(aliases: &[(&str, &str)]) -> Self { + Self { + aliases: aliases + .iter() + .map(|(old, new)| (old.to_string(), new.to_string())) + .collect(), + conversions: 0, + } + } + + /// Rewrite `Spring.OldName` to the canonical name wherever the prefix is + /// the bare `Spring` global. + pub fn rewrite(&mut self, source: &str, tree: &LuaSyntaxTree) -> String { + let mut edits: Vec = Vec::new(); + for node in tree.get_chunk_node().syntax().descendants() { + let Some(index) = LuaIndexExpr::cast(node) else { + continue; + }; + if is_func_stat_name(index.syntax()) { + continue; + } + let Some(LuaExpr::NameExpr(prefix)) = index.get_prefix_expr() else { + continue; + }; + if prefix.get_name_text().as_deref() != Some("Spring") { + continue; + } + let Some(LuaIndexKey::Name(name)) = index.get_index_key() else { + continue; + }; + let Some(canonical) = self.aliases.get(name.get_name_text()) else { + continue; + }; + self.conversions += 1; + let range = name.get_range(); + edits.push(Edit { + start: usize::from(range.start()), + end: usize::from(range.end()), + text: canonical.clone(), + }); + } + edit::apply(source, edits) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cst::parse; + + const ALIASES: &[(&str, &str)] = &[ + ("GetMyTeamID", "GetLocalTeamID"), + ("GetMyAllyTeamID", "GetLocalAllyTeamID"), + ("GetMyPlayerID", "GetLocalPlayerID"), + ]; + + fn transform(input: &str) -> (String, usize) { + let tree = parse(input).expect("parse failed"); + let mut visitor = RenameAliases::new(ALIASES); + let out = visitor.rewrite(input, &tree); + (out, visitor.conversions) + } + + #[test] + fn renames_call() { + let (out, n) = transform("local t = Spring.GetMyTeamID()"); + assert_eq!(out, "local t = Spring.GetLocalTeamID()"); + assert_eq!(n, 1); + } + + #[test] + fn renames_var_reference() { + let (out, n) = transform("local fn = Spring.GetMyAllyTeamID"); + assert_eq!(out, "local fn = Spring.GetLocalAllyTeamID"); + assert_eq!(n, 1); + } + + #[test] + fn non_alias_unchanged() { + let (out, n) = transform("Spring.GetGameFrame()"); + assert_eq!(out, "Spring.GetGameFrame()"); + assert_eq!(n, 0); + } + + #[test] + fn non_spring_unchanged() { + let (out, n) = transform("Other.GetMyTeamID()"); + assert_eq!(out, "Other.GetMyTeamID()"); + assert_eq!(n, 0); + } + + #[test] + fn preserves_trivia() { + let (out, n) = transform(" local id = Spring.GetMyPlayerID() -- get player"); + assert_eq!(out, " local id = Spring.GetLocalPlayerID() -- get player"); + assert_eq!(n, 1); + } + + #[test] + fn multiple_in_one_file() { + let input = "local a = Spring.GetMyTeamID()\nlocal b = Spring.GetMyAllyTeamID()"; + let (out, n) = transform(input); + assert!(out.contains("Spring.GetLocalTeamID()")); + assert!(out.contains("Spring.GetLocalAllyTeamID()")); + assert_eq!(n, 2); + } + + #[test] + fn bracket_access_unchanged() { + let (out, n) = transform(r#"local f = Spring["GetMyTeamID"]"#); + assert_eq!(out, r#"local f = Spring["GetMyTeamID"]"#); + assert_eq!(n, 0); + } +} diff --git a/scripts/codemod-cargo.sh b/scripts/codemod-cargo.sh new file mode 100755 index 00000000..88504fe4 --- /dev/null +++ b/scripts/codemod-cargo.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Run cargo for bar-lua-codemod inside DEVTOOLS_DISTROBOX (bar-dev has rust/cargo). +set -euo pipefail + +DEVTOOLS_DIR="${DEVTOOLS_DIR:?DEVTOOLS_DIR must be set}" +CODEMOD_DIR="$DEVTOOLS_DIR/bar-lua-codemod" + +source "$DEVTOOLS_DIR/scripts/common.sh" + +enter_distrobox "$@" + +cd "$CODEMOD_DIR" +cargo "$@"