From 608c06699ab9daff996e84b141391b9d1970b683 Mon Sep 17 00:00:00 2001 From: Ketasaja Date: Sat, 15 Feb 2025 21:40:38 +0000 Subject: [PATCH] Add `types_output` (#163) * Add `types_output` Co-authored-by: pashleyy <64950564+passhley@users.noreply.github.com> * Update docs/config/options.md Co-authored-by: Sasial <44125644+sasial-dev@users.noreply.github.com> * Set default Co-authored-by: Sasial <44125644+sasial-dev@users.noreply.github.com> * Remove clone Co-authored-by: Sasial <44125644+sasial-dev@users.noreply.github.com> * Fix formatting * Fix formatting * Fix formatting * Add TypeScript output * Remove formatting change * Fix output * Make type optional * Add Selene lints * Refactoring * Lune tests * Add newline to Selene tests * Fix Lune tests * Wrap tagged enums in parentheses * Formatting * Formatting --------- Co-authored-by: pashleyy <64950564+passhley@users.noreply.github.com> Co-authored-by: Sasial <44125644+sasial-dev@users.noreply.github.com> --- cli/src/main.rs | 20 +++++ docs/config/options.md | 12 +++ zap/src/config.rs | 1 + zap/src/lib.rs | 6 ++ zap/src/output/luau/mod.rs | 1 + zap/src/output/luau/types.rs | 71 +++++++++++++++++ zap/src/output/typescript/mod.rs | 1 + zap/src/output/typescript/types.rs | 76 +++++++++++++++++++ zap/src/parser/convert.rs | 17 +++++ zap/tests/lune/base.luau | 8 +- zap/tests/lune/mod.rs | 6 +- zap/tests/selene/mod.rs | 21 ++++- .../run_selene_test@function@types.snap | 6 ++ ...lene_test@function_mutiple_rets@types.snap | 6 ++ ...@function_one_unnamed_parameter@types.snap | 6 ++ ...function_two_unnamed_parameters@types.snap | 6 ++ .../run_selene_test@many_assorted@types.snap | 6 ++ .../run_selene_test@many_reliables@types.snap | 6 ++ ...un_selene_test@many_unreliables@types.snap | 6 ++ .../run_selene_test@nested_complex@types.snap | 6 ++ ...lene_test@one_unnamed_parameter@types.snap | 6 ++ .../run_selene_test@regression_153@types.snap | 6 ++ .../run_selene_test@simple@types.snap | 6 ++ .../run_selene_test@simple_struct@types.snap | 6 ++ .../run_selene_test@tuples@types.snap | 6 ++ ...ene_test@two_unnamed_parameters@types.snap | 6 ++ 26 files changed, 319 insertions(+), 5 deletions(-) create mode 100644 zap/src/output/luau/types.rs create mode 100644 zap/src/output/typescript/types.rs create mode 100644 zap/tests/selene/snapshots/run_selene_test@function@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@function_mutiple_rets@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@function_one_unnamed_parameter@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@function_two_unnamed_parameters@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@many_assorted@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@many_reliables@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@many_unreliables@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@nested_complex@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@one_unnamed_parameter@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@regression_153@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@simple@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@simple_struct@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@tuples@types.snap create mode 100644 zap/tests/selene/snapshots/run_selene_test@two_unnamed_parameters@types.snap diff --git a/cli/src/main.rs b/cli/src/main.rs index 31badf16..af0509d4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -69,6 +69,26 @@ fn main() -> Result<()> { std::fs::write(file_path, defs)?; } + if let Some(types_output) = code.types { + let types_path = config_path.parent().unwrap().join(types_output.path); + + if let Some(parent) = types_path.parent() { + std::fs::create_dir_all(parent)?; + } + + if let Some(defs) = types_output.defs { + let defs_path = if types_path.file_stem().unwrap() == "init" { + types_path.with_file_name("index.d.ts") + } else { + types_path.with_extension("d.ts") + }; + + std::fs::write(defs_path, defs)?; + } + + std::fs::write(types_path, types_output.code)?; + } + if let Some(tooling) = code.tooling { let tooling_path = config_path.parent().unwrap().join(tooling.path); diff --git a/docs/config/options.md b/docs/config/options.md index 17cd6f2d..4dfb856d 100644 --- a/docs/config/options.md +++ b/docs/config/options.md @@ -5,6 +5,8 @@ opt client_output = "path/to/client/output.lua"` const outputExample = `opt server_output = "./network/client.luau" opt client_output = "src/client/zap.luau"` +const typesOutputExample = `opt types_output = "network/types.luau"` + const asyncLibExample = `opt yield_type = "promise" opt async_lib = "require(game:GetService('ReplicatedStorage').Promise)"` @@ -37,6 +39,16 @@ The paths are relative to the configuration file and should point to a lua(u) fi +## `types_output` [`0.6.18+`] + +Configures where Luau types will be output. + +The path is relative to the configuration file and should point to a lua(u) file. + +### Example + + + ## `remote_scope` This option changes the name of the remotes generated by Zap. diff --git a/zap/src/config.rs b/zap/src/config.rs index f1cfc050..525dd311 100644 --- a/zap/src/config.rs +++ b/zap/src/config.rs @@ -23,6 +23,7 @@ pub struct Config<'src> { pub server_output: &'src str, pub client_output: &'src str, + pub types_output: Option<&'src str>, pub tooling_output: &'src str, pub casing: Casing, diff --git a/zap/src/lib.rs b/zap/src/lib.rs index 73188c29..a9e120d8 100644 --- a/zap/src/lib.rs +++ b/zap/src/lib.rs @@ -39,6 +39,7 @@ pub struct Output { pub struct Code { pub server: Output, pub client: Output, + pub types: Option, pub tooling: Option, } @@ -79,6 +80,11 @@ pub fn run(input: &str, no_warnings: bool) -> Return { code: output::luau::client::code(&config), defs: output::typescript::client::code(&config), }, + types: config.types_output.map(|types_output| Output { + path: types_output.into(), + code: output::luau::types::code(&config), + defs: output::typescript::types::code(&config), + }), tooling: output::tooling::output(&config), }), diagnostics, diff --git a/zap/src/output/luau/mod.rs b/zap/src/output/luau/mod.rs index fd4bddf5..801dca3e 100644 --- a/zap/src/output/luau/mod.rs +++ b/zap/src/output/luau/mod.rs @@ -5,6 +5,7 @@ use crate::{ pub mod client; pub mod server; +pub mod types; pub trait Output { fn push(&mut self, s: &str); diff --git a/zap/src/output/luau/types.rs b/zap/src/output/luau/types.rs new file mode 100644 index 00000000..5fb0dc2c --- /dev/null +++ b/zap/src/output/luau/types.rs @@ -0,0 +1,71 @@ +use crate::config::{Config, TyDecl}; + +use super::Output; + +struct TypesOutput<'src> { + config: &'src Config<'src>, + tabs: u32, + buf: String, +} + +impl Output for TypesOutput<'_> { + fn push(&mut self, s: &str) { + self.buf.push_str(s); + } + + fn indent(&mut self) { + self.tabs += 1; + } + + fn dedent(&mut self) { + self.tabs -= 1; + } + + fn push_indent(&mut self) { + for _ in 0..self.tabs { + self.push("\t"); + } + } +} + +impl<'a> TypesOutput<'a> { + pub fn new(config: &'a Config) -> Self { + Self { + config, + tabs: 0, + buf: String::new(), + } + } + + fn push_tydecl(&mut self, tydecl: &TyDecl) { + let name = &tydecl.name; + let ty = &tydecl.ty; + + self.push_indent(); + self.push(&format!("export type {name} = ")); + self.push_ty(ty); + self.push("\n"); + } + + fn push_tydecls(&mut self) { + for tydecl in self.config.tydecls.iter() { + self.push_tydecl(tydecl); + } + } + + pub fn output(mut self) -> String { + self.push_line(&format!( + "-- Types generated by Zap v{} (https://github.com/red-blox/zap)", + env!("CARGO_PKG_VERSION") + )); + + self.push_tydecls(); + self.push_line("return nil"); + + self.buf + } +} + +pub fn code(config: &Config) -> String { + TypesOutput::new(config).output() +} diff --git a/zap/src/output/typescript/mod.rs b/zap/src/output/typescript/mod.rs index f713de3d..670daec3 100644 --- a/zap/src/output/typescript/mod.rs +++ b/zap/src/output/typescript/mod.rs @@ -2,6 +2,7 @@ use crate::config::{Config, Enum, Parameter, Ty}; pub mod client; pub mod server; +pub mod types; pub trait ConfigProvider { fn get_config(&self) -> &Config; diff --git a/zap/src/output/typescript/types.rs b/zap/src/output/typescript/types.rs new file mode 100644 index 00000000..bfdbc23e --- /dev/null +++ b/zap/src/output/typescript/types.rs @@ -0,0 +1,76 @@ +use crate::config::{Config, TyDecl}; + +use super::{ConfigProvider, Output}; + +struct TypesOutput<'src> { + config: &'src Config<'src>, + tabs: u32, + buf: String, +} + +impl Output for TypesOutput<'_> { + fn push(&mut self, s: &str) { + self.buf.push_str(s); + } + + fn indent(&mut self) { + self.tabs += 1; + } + + fn dedent(&mut self) { + self.tabs -= 1; + } + + fn push_indent(&mut self) { + for _ in 0..self.tabs { + self.push("\t"); + } + } +} + +impl ConfigProvider for TypesOutput<'_> { + fn get_config(&self) -> &Config { + self.config + } +} + +impl<'a> TypesOutput<'a> { + pub fn new(config: &'a Config) -> Self { + Self { + config, + tabs: 0, + buf: String::new(), + } + } + + fn push_tydecl(&mut self, tydecl: &TyDecl) { + let name = &tydecl.name; + let ty = &tydecl.ty; + + self.push_indent(); + self.push(&format!("export type {name} = ")); + self.push_ty(ty); + self.push("\n"); + } + + fn push_tydecls(&mut self) { + for tydecl in self.config.tydecls.iter() { + self.push_tydecl(tydecl); + } + } + + pub fn output(mut self) -> String { + self.push_line(&format!( + "// Types generated by Zap v{} (https://github.com/red-blox/zap)", + env!("CARGO_PKG_VERSION") + )); + + self.push_tydecls(); + + self.buf + } +} + +pub fn code(config: &Config) -> Option { + Some(TypesOutput::new(config).output()) +} diff --git a/zap/src/parser/convert.rs b/zap/src/parser/convert.rs index 62745784..4d18c6c8 100644 --- a/zap/src/parser/convert.rs +++ b/zap/src/parser/convert.rs @@ -124,6 +124,7 @@ impl<'src> Converter<'src> { let (server_output, ..) = self.str_opt("server_output", "network/server.lua", &config.opts); let (client_output, ..) = self.str_opt("client_output", "network/client.lua", &config.opts); + let types_output: Option<&str> = self.types_output_opt(&config.opts); let (tooling_output, ..) = self.str_opt("tooling_output", "network/tooling.lua", &config.opts); let casing = self.casing_opt(&config.opts); @@ -150,6 +151,7 @@ impl<'src> Converter<'src> { server_output, client_output, + types_output, tooling_output, casing, @@ -233,6 +235,21 @@ impl<'src> Converter<'src> { } } + fn types_output_opt(&mut self, opts: &[SyntaxOpt<'src>]) -> Option<&'src str> { + let opt = opts.iter().find(|opt| opt.name.name == "types_output")?; + + if let SyntaxOptValueKind::Str(opt_value) = &opt.value.kind { + Some(self.str(opt_value)) + } else { + self.report(Report::AnalyzeInvalidOptValue { + span: opt.value.span(), + expected: "Types output path expected.", + }); + + None + } + } + fn boolean_opt(&mut self, name: &'static str, default: bool, opts: &[SyntaxOpt<'src>]) -> (bool, Option) { let mut value = default; let mut span = None; diff --git a/zap/tests/lune/base.luau b/zap/tests/lune/base.luau index 12940366..f252a34a 100644 --- a/zap/tests/lune/base.luau +++ b/zap/tests/lune/base.luau @@ -3,7 +3,7 @@ local luau = require("@lune/luau") local process = require("@lune/process") local roblox = require("@lune/roblox") local task = require("@lune/task") -local serverCode, clientCode, toolingCode = unpack(process.args) +local serverCode, clientCode, typesCode, toolingCode = unpack(process.args) local noop = function() end @@ -80,6 +80,12 @@ local client = luau.load(clientCode, { environment = environment })() +local types = luau.load(typesCode, { + debugName = "Types", + codegenEnabled = true, + environment = environment, +})() + local tooling = luau.load(toolingCode, { debugName = "Tooling", codegenEnabled = true, diff --git a/zap/tests/lune/mod.rs b/zap/tests/lune/mod.rs index 31679b58..472cf53f 100644 --- a/zap/tests/lune/mod.rs +++ b/zap/tests/lune/mod.rs @@ -4,7 +4,10 @@ use insta::{assert_debug_snapshot, glob, Settings}; use lune::Runtime; pub fn run_lune_test(input: &str, no_warnings: bool, insta_settings: Settings) { - let script = zap::run(&format!("opt tooling = true\n{input}"), no_warnings); + let script = zap::run( + &format!("opt tooling = true\nopt types_output = \"network/types.luau\"\n{input}"), + no_warnings, + ); assert!(script.code.is_some(), "No code generated!"); @@ -12,6 +15,7 @@ pub fn run_lune_test(input: &str, no_warnings: bool, insta_settings: Settings) { let mut runtime = Runtime::new().with_args(vec![ &code.server.code, &code.client.code, + &code.types.as_ref().unwrap().code, &code.tooling.as_ref().unwrap().code, ]); diff --git a/zap/tests/selene/mod.rs b/zap/tests/selene/mod.rs index b1a8d65b..52a78767 100644 --- a/zap/tests/selene/mod.rs +++ b/zap/tests/selene/mod.rs @@ -9,9 +9,12 @@ use selene_lib::{lints::Severity, CheckerDiagnostic}; static SELENE: LazyLock = LazyLock::new(initialise_selene); pub fn run_selene_test(input: &str, no_warnings: bool, insta_settings: &mut Settings, file_stem: Cow<'_, str>) { - let code = zap::run(&format!("opt tooling = true\n{input}"), no_warnings) - .code - .expect("Zap did not generate any code!"); + let code = zap::run( + &format!("opt tooling = true\nopt types_output = \"network/types.luau\"\n{input}"), + no_warnings, + ) + .code + .expect("Zap did not generate any code!"); let client_ast = full_moon::parse_fallible(&code.client.code, SELENE.lua_version).into_ast(); let client_diagnostics = SELENE @@ -39,6 +42,18 @@ pub fn run_selene_test(input: &str, no_warnings: bool, insta_settings: &mut Sett assert_debug_snapshot!(server_diagnostics); }); + let types_ast = full_moon::parse_fallible(&code.types.as_ref().unwrap().code, SELENE.lua_version).into_ast(); + let types_diagnostics = SELENE + .linter + .test_on(&types_ast) + .into_iter() + .filter(|diagnostic| diagnostic.severity != Severity::Allow) + .collect::>(); + insta_settings.set_snapshot_suffix(format!("{file_stem}@types")); + insta_settings.bind(|| { + assert_debug_snapshot!(types_diagnostics); + }); + let tooling_ast = full_moon::parse_fallible(&code.tooling.as_ref().unwrap().code, SELENE.lua_version).into_ast(); let tooling_diagnostics = SELENE .linter diff --git a/zap/tests/selene/snapshots/run_selene_test@function@types.snap b/zap/tests/selene/snapshots/run_selene_test@function@types.snap new file mode 100644 index 00000000..9a7fb58f --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@function@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/function.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@function_mutiple_rets@types.snap b/zap/tests/selene/snapshots/run_selene_test@function_mutiple_rets@types.snap new file mode 100644 index 00000000..b307bf17 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@function_mutiple_rets@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/function_mutiple_rets.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@function_one_unnamed_parameter@types.snap b/zap/tests/selene/snapshots/run_selene_test@function_one_unnamed_parameter@types.snap new file mode 100644 index 00000000..261273c3 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@function_one_unnamed_parameter@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/function_one_unnamed_parameter.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@function_two_unnamed_parameters@types.snap b/zap/tests/selene/snapshots/run_selene_test@function_two_unnamed_parameters@types.snap new file mode 100644 index 00000000..6f3589ef --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@function_two_unnamed_parameters@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/function_two_unnamed_parameters.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@many_assorted@types.snap b/zap/tests/selene/snapshots/run_selene_test@many_assorted@types.snap new file mode 100644 index 00000000..288c4dca --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@many_assorted@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/many_assorted.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@many_reliables@types.snap b/zap/tests/selene/snapshots/run_selene_test@many_reliables@types.snap new file mode 100644 index 00000000..6a1817d3 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@many_reliables@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/many_reliables.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@many_unreliables@types.snap b/zap/tests/selene/snapshots/run_selene_test@many_unreliables@types.snap new file mode 100644 index 00000000..40c545dc --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@many_unreliables@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/many_unreliables.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@nested_complex@types.snap b/zap/tests/selene/snapshots/run_selene_test@nested_complex@types.snap new file mode 100644 index 00000000..031e8bc1 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@nested_complex@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/nested_complex.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@one_unnamed_parameter@types.snap b/zap/tests/selene/snapshots/run_selene_test@one_unnamed_parameter@types.snap new file mode 100644 index 00000000..cd8d9378 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@one_unnamed_parameter@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/one_unnamed_parameter.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@regression_153@types.snap b/zap/tests/selene/snapshots/run_selene_test@regression_153@types.snap new file mode 100644 index 00000000..dca8ffa4 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@regression_153@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/regression_153.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@simple@types.snap b/zap/tests/selene/snapshots/run_selene_test@simple@types.snap new file mode 100644 index 00000000..7d1892e8 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@simple@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/simple.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@simple_struct@types.snap b/zap/tests/selene/snapshots/run_selene_test@simple_struct@types.snap new file mode 100644 index 00000000..41b10e60 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@simple_struct@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/simple_struct.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@tuples@types.snap b/zap/tests/selene/snapshots/run_selene_test@tuples@types.snap new file mode 100644 index 00000000..687b9662 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@tuples@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/tuples.zap +--- +[] diff --git a/zap/tests/selene/snapshots/run_selene_test@two_unnamed_parameters@types.snap b/zap/tests/selene/snapshots/run_selene_test@two_unnamed_parameters@types.snap new file mode 100644 index 00000000..55f931a9 --- /dev/null +++ b/zap/tests/selene/snapshots/run_selene_test@two_unnamed_parameters@types.snap @@ -0,0 +1,6 @@ +--- +source: zap/tests/selene/mod.rs +expression: types_diagnostics +input_file: zap/tests/files/two_unnamed_parameters.zap +--- +[]