From 0657e05cc0a03df99b9d2759642f4b677d57ae1e Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Thu, 26 Jan 2023 19:36:06 -0500 Subject: [PATCH] Implement docs file generation & docs in the luau type definitions file --- .github/workflows/release.yaml | 10 ++ .vscode/settings.json | 1 + Cargo.lock | 181 +++++++++++++++++++ Cargo.toml | 2 + README.md | 110 ++---------- luneDocs.json | 305 +++++++++++++++++++++++++++++++++ luneTypes.d.luau | 251 +++++++++++++++++++++++++++ src/cli/cli.rs | 32 +++- src/cli/gen/doc.rs | 46 +++++ src/cli/gen/mod.rs | 83 +++++++++ src/cli/gen/tag.rs | 64 +++++++ src/cli/gen/visitor.rs | 188 ++++++++++++++++++++ src/cli/main.rs | 7 +- 13 files changed, 1175 insertions(+), 105 deletions(-) create mode 100644 luneDocs.json create mode 100644 src/cli/gen/doc.rs create mode 100644 src/cli/gen/mod.rs create mode 100644 src/cli/gen/tag.rs create mode 100644 src/cli/gen/visitor.rs diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 143e7c6..7eee2bf 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -79,6 +79,16 @@ jobs: asset_name: luneTypes.d.luau asset_content_type: application/x-luau + - name: Upload documentation file to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: luneDocs.json + asset_name: luneDocs.json + asset_content_type: application/json + release: needs: ["init", "create-release"] strategy: diff --git a/.vscode/settings.json b/.vscode/settings.json index 04aaefb..4b0eb13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "luau-lsp.sourcemap.enabled": false, "luau-lsp.types.roblox": false, "luau-lsp.types.definitionFiles": ["luneTypes.d.luau"], + "luau-lsp.types.documentationFiles": ["luneDocs.json"], "luau-lsp.require.mode": "relativeToFile", // Rust "rust-analyzer.check.command": "clippy", diff --git a/Cargo.lock b/Cargo.lock index 564eaea..c9fc97d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.68" @@ -135,6 +144,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bitflags" version = "1.3.2" @@ -170,6 +185,12 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "bytecount" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0fdd54b507df8f22012890aadd099979befdba27713c767993f8380112ca7c" + [[package]] name = "cc" version = "1.0.78" @@ -228,6 +249,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "crc32fast" version = "1.3.2" @@ -246,6 +273,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "erased-serde" version = "0.3.24" @@ -301,6 +341,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.1.0" @@ -310,6 +356,34 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "full_moon" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d58cb343df2e63e8a496de3e344e5f2b97f010bc1359e994be38a99586888f5" +dependencies = [ + "bytecount", + "cfg-if", + "derive_more", + "full_moon_derive", + "logos", + "paste", + "serde", + "smol_str", +] + +[[package]] +name = "full_moon_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b4bd12ce56927d1dc5478d21528ea8c4b93ca85ff8f8043b6a5351a2a3c6f7" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-core" version = "0.3.25" @@ -368,6 +442,12 @@ dependencies = [ "slab", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "heck" version = "0.4.0" @@ -393,6 +473,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.12" @@ -460,6 +550,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "logos" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax", + "syn", +] + [[package]] name = "luau0-src" version = "0.5.1+luau558" @@ -475,8 +588,10 @@ version = "0.1.3" dependencies = [ "anyhow", "clap", + "full_moon", "mlua", "os_str_bytes", + "regex", "serde", "serde_json", "smol", @@ -548,6 +663,25 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -610,6 +744,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.50" @@ -628,6 +768,23 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + [[package]] name = "ring" version = "0.16.20" @@ -649,6 +806,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.36.7" @@ -691,6 +857,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "semver" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" + [[package]] name = "serde" version = "1.0.152" @@ -767,6 +939,15 @@ dependencies = [ "futures-lite", ] +[[package]] +name = "smol_str" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7475118a28b7e3a2e157ce0131ba8c5526ea96e90ee601d9f6bb2e286a35ab44" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.4.7" diff --git a/Cargo.toml b/Cargo.toml index 46a6f5a..bc97447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,12 @@ panic = "abort" # Remove extra panic info anyhow = "1.0.68" os_str_bytes = "6.4.1" +regex = "1.7.1" serde_json = "1.0.91" smol = "1.3.0" ureq = "2.6.2" clap = { version = "4.1.1", features = ["derive"] } +full_moon = { version = "0.17.0", features = ["roblox"] } mlua = { version = "0.8.7", features = ["luau", "async", "serialize"] } serde = { version = "1.0.152", features = ["derive"] } diff --git a/README.md b/README.md index 9c8d2b2..02ecd2f 100644 --- a/README.md +++ b/README.md @@ -33,102 +33,15 @@ Check out the examples on how to write a script in the [.lune](.lune) folder ! < A great starting point and walkthrough of Lune can be found in the [Hello, Lune](.lune/hello_lune.luau) example.
-🔎 Full list of APIs +🔎 List of APIs -
-console - Logging & formatting +`console` - Logging & formatting
+`fs` - Filesystem
+`net` - Networking
+`process` - Current process & child processes
+`task` - Task scheduler & thread spawning
-```lua -type console = { - resetColor: () -> (), - setColor: (color: "black" | "red" | "green" | "yellow" | "blue" | "purple" | "cyan" | "white") -> (), - resetStyle: () -> (), - setStyle: (color: "bold" | "dim") -> (), - format: (...any) -> (string), - log: (...any) -> (), - info: (...any) -> (), - warn: (...any) -> (), - error: (...any) -> (), -} -``` - -
- -
-fs - Filesystem - -```lua -type fs = { - readFile: (path: string) -> string, - readDir: (path: string) -> { string }, - writeFile: (path: string, contents: string) -> (), - writeDir: (path: string) -> (), - removeFile: (path: string) -> (), - removeDir: (path: string) -> (), - isFile: (path: string) -> boolean, - isDir: (path: string) -> boolean, -} -``` - -
- -
-net - Networking - -```lua -type net = { - request: (config: string | { - url: string, - method: ("GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH")?, - headers: { [string]: string }?, - body: string?, - }) -> { - ok: boolean, - statusCode: number, - statusMessage: string, - headers: { [string]: string }, - body: string, - }, - jsonEncode: (value: any, pretty: boolean?) -> string, - jsonDecode: (encoded: string) -> any, -} -``` - -
- -
-process - Current process & child processes - -```lua -type process = { - args: { string }, - env: { [string]: string? }, - exit: (code: number?) -> (), - spawn: (program: string, params: { string }?) -> { - ok: boolean, - code: number, - stdout: string, - stderr: string, - }, -} -``` - -
- -
-task - Task scheduler & thread spawning - -```lua -type task = { - cancel: (thread: thread) -> (), - defer: (functionOrThread: thread | (T...) -> (...any), T...) -> thread, - delay: (duration: number?, functionOrThread: thread | (T...) -> (...any), T...) -> thread, - spawn: (functionOrThread: thread | (T...) -> (...any), T...) -> thread, - wait: (duration: number?) -> (number), -} -``` - -
+Documentation for individual members and types can be found using your editor of choice and [Luau LSP](https://github.com/JohnnyMorganz/luau-lsp).
@@ -174,9 +87,12 @@ Lune puts developer experience first, and as such provides type definitions and
Luau LSP -1. Use `lune --download-luau-types` to download Luau types (`luneTypes.d.luau`) to the current directory -2. Set your definition files setting to include `luneTypes.d.luau` -3. Set the require mode setting to `relativeToFile` +1. Set the require mode setting to `relativeToFile` +2. Use `lune --download-luau-types` to download Luau types (`luneTypes.d.luau`) to the current directory +3. Set your definition files setting to include `luneTypes.d.luau` +4. Generate the documentation file using `lune --generate-docs-file` + - NOTE: This is a temporary solution and a docs file separate from type definitions will not be necessary in the future +5. Set your documentation files setting to include `luneDocs.json` An example of these settings can be found in the [.vscode](.vscode) folder in this repository diff --git a/luneDocs.json b/luneDocs.json new file mode 100644 index 0000000..0d731fd --- /dev/null +++ b/luneDocs.json @@ -0,0 +1,305 @@ +{ + "@roblox/global/console": { + "code_sample": "", + "documentation": "Logging & formatting", + "keys": { + "console": "@roblox/global/console.console" + }, + "learn_more_link": "" + }, + "@roblox/global/console.error": { + "code_sample": "", + "documentation": "Prints arguments as a human-readable string with syntax highlighting for tables to stderr.\n\nThis will also prepend an [ERROR] tag at the beginning of the message.\n\nUsing this function will automatically set the exit code of the process\nto 1, unless it gets manually specified afterwards using `process.exit`.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/console.format": { + "code_sample": "", + "documentation": "Formats arguments into a human-readable string with syntax highlighting for tables.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/console.info": { + "code_sample": "", + "documentation": "Prints arguments as a human-readable string with syntax highlighting for tables to stdout.\n\nThis will also prepend an [INFO] tag at the beginning of the message.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/console.log": { + "code_sample": "", + "documentation": "Prints arguments as a human-readable string with syntax highlighting for tables to stdout.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/console.resetColor": { + "code_sample": "", + "documentation": "Resets the current persistent output color.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/console.resetStyle": { + "code_sample": "", + "documentation": "Resets the current persistent output style.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/console.setColor": { + "code_sample": "", + "documentation": "Sets the current persistent output color.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/console.setStyle": { + "code_sample": "", + "documentation": "Sets the current persistent output style.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/console.warn": { + "code_sample": "", + "documentation": "Prints arguments as a human-readable string with syntax highlighting for tables to stdout.\n\nThis will also prepend an [INFO] tag at the beginning of the message.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/fs": { + "code_sample": "", + "documentation": "Filesystem", + "keys": { + "fs": "@roblox/global/fs.fs" + }, + "learn_more_link": "" + }, + "@roblox/global/fs.isDir": { + "code_sample": "", + "documentation": "Checks if a given path is a directory.\n\nAn error will be thrown in the following situations:\n\n* The current process lacks permissions to read at `path`.\n* Some other I/O error occurred.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/fs.isFile": { + "code_sample": "", + "documentation": "Checks if a given path is a file.\n\nAn error will be thrown in the following situations:\n\n* The current process lacks permissions to read at `path`.\n* Some other I/O error occurred.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/fs.readDir": { + "code_sample": "", + "documentation": "Reads entries in a directory at `path`.\n\nAn error will be thrown in the following situations:\n\n* `path` does not point to an existing directory.\n* The current process lacks permissions to read the contents of the directory.\n* Some other I/O error occurred.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/fs.readFile": { + "code_sample": "", + "documentation": "Reads a file at `path`.\n\nAn error will be thrown in the following situations:\n\n* `path` does not point to an existing file.\n* The current process lacks permissions to read the file.\n* The contents of the file cannot be read as a UTF-8 string.\n* Some other I/O error occurred.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/fs.removeDir": { + "code_sample": "", + "documentation": "Removes a directory and all of its contents.\n\nAn error will be thrown in the following situations:\n\n* `path` is not an existing and empty directory.\n* The current process lacks permissions to remove the directory.\n* Some other I/O error occurred.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/fs.removeFile": { + "code_sample": "", + "documentation": "Removes a file.\n\nAn error will be thrown in the following situations:\n\n* `path` does not point to an existing file.\n* The current process lacks permissions to remove the file.\n* Some other I/O error occurred.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/fs.writeDir": { + "code_sample": "", + "documentation": "Creates a directory and its parent directories if they are missing.\n\nAn error will be thrown in the following situations:\n\n* `path` already points to an existing file or directory.\n* The current process lacks permissions to create the directory or its missing parents.\n* Some other I/O error occurred.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/fs.writeFile": { + "code_sample": "", + "documentation": "Writes to a file at `path`.\n\nAn error will be thrown in the following situations:\n\n* The file's parent directory does not exist.\n* The current process lacks permissions to write to the file.\n* Some other I/O error occurred.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/net": { + "code_sample": "", + "documentation": "Networking", + "keys": { + "net": "@roblox/global/net.net" + }, + "learn_more_link": "" + }, + "@roblox/global/net.jsonDecode": { + "code_sample": "", + "documentation": "Decodes the given JSON string into a lua value.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/net.jsonEncode": { + "code_sample": "", + "documentation": "Encodes the given value as JSON.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/net.request": { + "code_sample": "", + "documentation": "Sends an HTTP request using the given url and / or parameters, and returns a dictionary that describes the response received.\n\nOnly throws an error if a miscellaneous network or I/O error occurs, never for unsuccessful status codes.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/process": { + "code_sample": "", + "documentation": "Current process & child processes", + "keys": { + "process": "@roblox/global/process.process" + }, + "learn_more_link": "" + }, + "@roblox/global/process.args": { + "code_sample": "", + "documentation": "The arguments given when running the Lune script.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/process.env": { + "code_sample": "", + "documentation": "Current environment variables for this process.\n\nSetting a value on this table will set the corresponding environment variable.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/process.exit": { + "code_sample": "", + "documentation": "Exits the currently running script as soon as possible with the given exit code.\n\nExit code 0 is treated as a successful exit, any other value is treated as an error.\n\nSetting the exit code using this function will override any otherwise automatic exit code.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/process.spawn": { + "code_sample": "", + "documentation": "Spawns a child process that will run the program `program` with the given `params` as arguments, and returns a dictionary that describes the final status and ouput of the child process.", + "learn_more_link": "", + "params": [], + "returns": [] + }, + "@roblox/global/task": { + "code_sample": "", + "documentation": "Task scheduler & thread spawning", + "keys": { + "task": "@roblox/global/task.task" + }, + "learn_more_link": "" + }, + "@roblox/global/task.cancel": { + "code_sample": "", + "documentation": "Stops a currently scheduled thread from resuming.", + "learn_more_link": "", + "params": [ + { + "documentation": "@roblox/global/task.cancel/param/0", + "name": "thread" + } + ], + "returns": [] + }, + "@roblox/global/task.cancel/param/0": { + "documentation": "The thread to cancel" + }, + "@roblox/global/task.defer": { + "code_sample": "", + "documentation": "Defers a thread or function to run at the end of the current task queue.", + "learn_more_link": "", + "params": [ + { + "documentation": "@roblox/global/task.defer/param/0", + "name": "functionOrThread" + } + ], + "returns": [ + "@roblox/global/task.defer/return/0" + ] + }, + "@roblox/global/task.defer/param/0": { + "documentation": "The function or thread to defer" + }, + "@roblox/global/task.defer/return/0": { + "documentation": "The thread that will be deferred" + }, + "@roblox/global/task.delay": { + "code_sample": "", + "documentation": "Delays a thread or function to run after `duration` seconds.", + "learn_more_link": "", + "params": [ + { + "documentation": "@roblox/global/task.delay/param/0", + "name": "functionOrThread" + } + ], + "returns": [ + "@roblox/global/task.delay/return/0" + ] + }, + "@roblox/global/task.delay/param/0": { + "documentation": "The function or thread to delay" + }, + "@roblox/global/task.delay/return/0": { + "documentation": "The thread that will be delayed" + }, + "@roblox/global/task.spawn": { + "code_sample": "", + "documentation": "Instantly runs a thread or function.\n\nIf the spawned task yields, the thread that spawned the task\nwill resume, letting the spawned task run in the background.", + "learn_more_link": "", + "params": [ + { + "documentation": "@roblox/global/task.spawn/param/0", + "name": "functionOrThread" + } + ], + "returns": [ + "@roblox/global/task.spawn/return/0" + ] + }, + "@roblox/global/task.spawn/param/0": { + "documentation": "The function or thread to spawn" + }, + "@roblox/global/task.spawn/return/0": { + "documentation": "The thread that was spawned" + }, + "@roblox/global/task.wait": { + "code_sample": "", + "documentation": "Waits for the given duration, with a minimum wait time of 10 milliseconds.", + "learn_more_link": "", + "params": [ + { + "documentation": "@roblox/global/task.wait/param/0", + "name": "duration" + } + ], + "returns": [ + "@roblox/global/task.wait/return/0" + ] + }, + "@roblox/global/task.wait/param/0": { + "documentation": "The amount of time to wait" + }, + "@roblox/global/task.wait/return/0": { + "documentation": "The exact amount of time waited" + } +} \ No newline at end of file diff --git a/luneTypes.d.luau b/luneTypes.d.luau index 4bd9542..a5b3ad3 100644 --- a/luneTypes.d.luau +++ b/luneTypes.d.luau @@ -1,29 +1,192 @@ -- Lune v0.1.3 +--[=[ + @class console + + Logging & formatting +]=] declare console: { + --[=[ + @within console + + Resets the current persistent output color. + ]=] resetColor: () -> (), + --[=[ + @within console + + Sets the current persistent output color. + ]=] setColor: (color: "black" | "red" | "green" | "yellow" | "blue" | "purple" | "cyan" | "white") -> (), + --[=[ + @within console + + Resets the current persistent output style. + ]=] resetStyle: () -> (), + --[=[ + @within console + + Sets the current persistent output style. + ]=] setStyle: (style: "bold" | "dim") -> (), + --[=[ + @within console + + Formats arguments into a human-readable string with syntax highlighting for tables. + ]=] format: (...any) -> (string), + --[=[ + @within console + + Prints arguments as a human-readable string with syntax highlighting for tables to stdout. + ]=] log: (...any) -> (), + --[=[ + @within console + + Prints arguments as a human-readable string with syntax highlighting for tables to stdout. + + This will also prepend an [INFO] tag at the beginning of the message. + ]=] info: (...any) -> (), + --[=[ + @within console + + Prints arguments as a human-readable string with syntax highlighting for tables to stdout. + + This will also prepend an [INFO] tag at the beginning of the message. + ]=] warn: (...any) -> (), + --[=[ + @within console + + Prints arguments as a human-readable string with syntax highlighting for tables to stderr. + + This will also prepend an [ERROR] tag at the beginning of the message. + + Using this function will automatically set the exit code of the process + to 1, unless it gets manually specified afterwards using `process.exit`. + ]=] error: (...any) -> (), } +--[=[ + @class fs + + Filesystem +]=] declare fs: { + --[=[ + @within fs + + Reads a file at `path`. + + An error will be thrown in the following situations: + + * `path` does not point to an existing file. + * The current process lacks permissions to read the file. + * The contents of the file cannot be read as a UTF-8 string. + * Some other I/O error occurred. + ]=] readFile: (path: string) -> string, + --[=[ + @within fs + + Reads entries in a directory at `path`. + + An error will be thrown in the following situations: + + * `path` does not point to an existing directory. + * The current process lacks permissions to read the contents of the directory. + * Some other I/O error occurred. + ]=] readDir: (path: string) -> { string }, + --[=[ + @within fs + + Writes to a file at `path`. + + An error will be thrown in the following situations: + + * The file's parent directory does not exist. + * The current process lacks permissions to write to the file. + * Some other I/O error occurred. + ]=] writeFile: (path: string, contents: string) -> (), + --[=[ + @within fs + + Creates a directory and its parent directories if they are missing. + + An error will be thrown in the following situations: + + * `path` already points to an existing file or directory. + * The current process lacks permissions to create the directory or its missing parents. + * Some other I/O error occurred. + ]=] writeDir: (path: string) -> (), + --[=[ + @within fs + + Removes a file. + + An error will be thrown in the following situations: + + * `path` does not point to an existing file. + * The current process lacks permissions to remove the file. + * Some other I/O error occurred. + ]=] removeFile: (path: string) -> (), + --[=[ + @within fs + + Removes a directory and all of its contents. + + An error will be thrown in the following situations: + + * `path` is not an existing and empty directory. + * The current process lacks permissions to remove the directory. + * Some other I/O error occurred. + ]=] removeDir: (path: string) -> (), + --[=[ + @within fs + + Checks if a given path is a file. + + An error will be thrown in the following situations: + + * The current process lacks permissions to read at `path`. + * Some other I/O error occurred. + ]=] isFile: (path: string) -> boolean, + --[=[ + @within fs + + Checks if a given path is a directory. + + An error will be thrown in the following situations: + + * The current process lacks permissions to read at `path`. + * Some other I/O error occurred. + ]=] isDir: (path: string) -> boolean, } +--[=[ + @class net + + Networking +]=] declare net: { + --[=[ + @within net + + Sends an HTTP request using the given url and / or parameters, and returns a dictionary that describes the response received. + + Only throws an error if a miscellaneous network or I/O error occurs, never for unsuccessful status codes. + ]=] request: (config: string | { url: string, method: ("GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH")?, @@ -36,14 +199,55 @@ declare net: { headers: { [string]: string }, body: string, }, + --[=[ + @within net + + Encodes the given value as JSON. + ]=] jsonEncode: (value: any, pretty: boolean?) -> string, + --[=[ + @within net + + Decodes the given JSON string into a lua value. + ]=] jsonDecode: (encoded: string) -> any, } +--[=[ + @class process + + Current process & child processes +]=] declare process: { + --[=[ + @within process + + The arguments given when running the Lune script. + ]=] args: { string }, + --[=[ + @within process + + Current environment variables for this process. + + Setting a value on this table will set the corresponding environment variable. + ]=] env: { [string]: string? }, + --[=[ + @within process + + Exits the currently running script as soon as possible with the given exit code. + + Exit code 0 is treated as a successful exit, any other value is treated as an error. + + Setting the exit code using this function will override any otherwise automatic exit code. + ]=] exit: (code: number?) -> (), + --[=[ + @within process + + Spawns a child process that will run the program `program` with the given `params` as arguments, and returns a dictionary that describes the final status and ouput of the child process. + ]=] spawn: (program: string, params: { string }?) -> { ok: boolean, code: number, @@ -52,10 +256,57 @@ declare process: { }, } +--[=[ + @class task + + Task scheduler & thread spawning +]=] declare task: { + --[=[ + @within task + + Stops a currently scheduled thread from resuming. + + @param thread The thread to cancel + ]=] cancel: (thread: thread) -> (), + --[=[ + @within task + + Defers a thread or function to run at the end of the current task queue. + + @param functionOrThread The function or thread to defer + @return The thread that will be deferred + ]=] defer: (functionOrThread: thread | (T...) -> (...any), T...) -> thread, + --[=[ + @within task + + Delays a thread or function to run after `duration` seconds. + + @param functionOrThread The function or thread to delay + @return The thread that will be delayed + ]=] delay: (duration: number?, functionOrThread: thread | (T...) -> (...any), T...) -> thread, + --[=[ + @within task + + Instantly runs a thread or function. + + If the spawned task yields, the thread that spawned the task + will resume, letting the spawned task run in the background. + + @param functionOrThread The function or thread to spawn + @return The thread that was spawned + ]=] spawn: (functionOrThread: thread | (T...) -> (...any), T...) -> thread, + --[=[ + @within task + + Waits for the given duration, with a minimum wait time of 10 milliseconds. + + @param duration The amount of time to wait + @return The exact amount of time waited + ]=] wait: (duration: number?) -> (number), } diff --git a/src/cli/cli.rs b/src/cli/cli.rs index a1359f9..643ce73 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -1,22 +1,28 @@ -use std::{fs::read_to_string, process::ExitCode}; +use std::process::ExitCode; use anyhow::Result; use clap::{CommandFactory, Parser}; use lune::Lune; +use smol::fs::{read_to_string, write}; -use crate::utils::{ - files::find_parse_file_path, - github::Client as GithubClient, - listing::{find_lune_scripts, print_lune_scripts, sort_lune_scripts}, +use crate::{ + gen::generate_docs_json_from_definitions, + utils::{ + files::find_parse_file_path, + github::Client as GithubClient, + listing::{find_lune_scripts, print_lune_scripts, sort_lune_scripts}, + }, }; const LUNE_SELENE_FILE_NAME: &str = "lune.yml"; const LUNE_LUAU_FILE_NAME: &str = "luneTypes.d.luau"; +const LUNE_DOCS_FILE_NAME: &str = "luneDocs.json"; /// Lune CLI #[derive(Parser, Debug, Default)] #[command(author, version, about, long_about = None)] +#[allow(clippy::struct_excessive_bools)] pub struct Cli { /// Path to the file to run, or the name /// of a luau file in a lune directory @@ -37,6 +43,10 @@ pub struct Cli { /// definitions file to the current directory #[clap(long)] download_luau_types: bool, + /// Pass this flag to generate the Lune documentation file + /// from a luau type definitions file in the current directory + #[clap(long)] + generate_docs_file: bool, } #[allow(dead_code)] @@ -123,10 +133,18 @@ impl Cli { .await?; } } + // Generate docs file, if wanted + if self.generate_docs_file { + let defs_contents = read_to_string(LUNE_LUAU_FILE_NAME).await?; + let docs_root = generate_docs_json_from_definitions(&defs_contents, "roblox/global")?; + let docs_contents = serde_json::to_string_pretty(&docs_root)?; + write(LUNE_DOCS_FILE_NAME, &docs_contents).await?; + } if self.script_path.is_none() { // Only downloading types without running a script is completely // fine, and we should just exit the program normally afterwards - if download_types_requested { + // Same thing goes for generating the docs file + if download_types_requested || self.generate_docs_file { return Ok(ExitCode::SUCCESS); } // HACK: We know that we didn't get any arguments here but since @@ -138,7 +156,7 @@ impl Cli { } // Parse and read the wanted file let file_path = find_parse_file_path(&self.script_path.unwrap())?; - let file_contents = read_to_string(&file_path)?; + let file_contents = read_to_string(&file_path).await?; // Display the file path relative to cwd with no extensions in stack traces let file_display_name = file_path.with_extension("").display().to_string(); // Create a new lune object with all globals & run the script diff --git a/src/cli/gen/doc.rs b/src/cli/gen/doc.rs new file mode 100644 index 0000000..af928fa --- /dev/null +++ b/src/cli/gen/doc.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct DocsGlobal { + pub documentation: String, + pub keys: HashMap, + pub learn_more_link: String, + pub code_sample: String, +} + +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct DocsFunctionParamLink { + pub name: String, + pub documentation: String, +} + +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct DocsFunction { + #[serde(skip)] + pub global_name: String, + pub documentation: String, + pub params: Vec, + pub returns: Vec, + pub learn_more_link: String, + pub code_sample: String, +} + +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct DocsParam { + #[serde(skip)] + pub global_name: String, + #[serde(skip)] + pub function_name: String, + pub documentation: String, +} + +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct DocsReturn { + #[serde(skip)] + pub global_name: String, + #[serde(skip)] + pub function_name: String, + pub documentation: String, +} diff --git a/src/cli/gen/mod.rs b/src/cli/gen/mod.rs new file mode 100644 index 0000000..5daa0b6 --- /dev/null +++ b/src/cli/gen/mod.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; + +use anyhow::Result; +use regex::Regex; +use serde_json::{Map, Value}; + +use full_moon::{parse as parse_luau_ast, visitors::Visitor}; + +mod doc; +mod tag; +mod visitor; + +use self::{doc::DocsFunctionParamLink, visitor::DocumentationVisitor}; + +fn parse_definitions(contents: &str) -> Result { + let (regex, replacement) = ( + Regex::new(r#"declare (?P\w+): \{"#).unwrap(), + r#"export type $n = {"#, + ); + let defs_ast = parse_luau_ast(®ex.replace_all(contents, replacement))?; + let mut visitor = DocumentationVisitor::new(); + visitor.visit_ast(&defs_ast); + Ok(visitor) +} + +pub fn generate_docs_json_from_definitions(contents: &str, namespace: &str) -> Result { + let visitor = parse_definitions(contents)?; + /* + Extract globals, functions, params, returns from the visitor + Here we will also convert the plain names into proper namespaced names according to the spec at + https://raw.githubusercontent.com/MaximumADHD/Roblox-Client-Tracker/roblox/api-docs/en-us.json + */ + let mut map = Map::new(); + for (name, mut doc) in visitor.globals { + doc.keys = doc + .keys + .iter() + .map(|(key, value)| (key.clone(), format!("@{namespace}/{name}.{value}"))) + .collect::>(); + map.insert(format!("@{namespace}/{name}"), serde_json::to_value(doc)?); + } + for (name, mut doc) in visitor.functions { + doc.params = doc + .params + .iter() + .map(|param| DocsFunctionParamLink { + name: param.name.clone(), + documentation: format!( + "@{namespace}/{}.{name}/param/{}", + doc.global_name, param.documentation + ), + }) + .collect::>(); + doc.returns = doc + .returns + .iter() + .map(|ret| format!("@{namespace}/{}.{name}/return/{ret}", doc.global_name)) + .collect::>(); + map.insert( + format!("@{namespace}/{}.{name}", doc.global_name), + serde_json::to_value(doc)?, + ); + } + for (name, doc) in visitor.params { + map.insert( + format!( + "@{namespace}/{}.{}/param/{name}", + doc.global_name, doc.function_name + ), + serde_json::to_value(doc)?, + ); + } + for (name, doc) in visitor.returns { + map.insert( + format!( + "@{namespace}/{}.{}/return/{name}", + doc.global_name, doc.function_name + ), + serde_json::to_value(doc)?, + ); + } + Ok(Value::Object(map)) +} diff --git a/src/cli/gen/tag.rs b/src/cli/gen/tag.rs new file mode 100644 index 0000000..a117604 --- /dev/null +++ b/src/cli/gen/tag.rs @@ -0,0 +1,64 @@ +use anyhow::{bail, Result}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DocsTagKind { + Class, + Within, + Param, + Return, +} + +impl DocsTagKind { + pub fn parse(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_ref() { + "class" => Ok(Self::Class), + "within" => Ok(Self::Within), + "param" => Ok(Self::Param), + "return" => Ok(Self::Return), + s => bail!("Unknown docs tag: '{}'", s), + } + } +} + +#[derive(Clone, Debug)] +pub struct DocsTag { + pub kind: DocsTagKind, + pub name: String, + pub contents: String, +} + +#[derive(Clone, Debug)] +pub struct DocsTagList { + tags: Vec, +} + +impl DocsTagList { + pub fn new() -> Self { + Self { tags: vec![] } + } + + pub fn push(&mut self, tag: DocsTag) { + self.tags.push(tag); + } + + pub fn contains(&mut self, kind: DocsTagKind) -> bool { + self.tags.iter().any(|tag| tag.kind == kind) + } + + pub fn find(&mut self, kind: DocsTagKind) -> Option<&DocsTag> { + self.tags.iter().find(|tag| tag.kind == kind) + } + + pub fn is_empty(&self) -> bool { + self.tags.is_empty() + } +} + +impl IntoIterator for DocsTagList { + type Item = DocsTag; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.tags.into_iter() + } +} diff --git a/src/cli/gen/visitor.rs b/src/cli/gen/visitor.rs new file mode 100644 index 0000000..44d4389 --- /dev/null +++ b/src/cli/gen/visitor.rs @@ -0,0 +1,188 @@ +use full_moon::{ + ast::types::{ExportedTypeDeclaration, TypeField, TypeFieldKey}, + tokenizer::{Token, TokenType}, + visitors::Visitor, +}; +use regex::Regex; + +use super::{ + doc::{DocsFunction, DocsFunctionParamLink, DocsGlobal, DocsParam, DocsReturn}, + tag::{DocsTag, DocsTagKind, DocsTagList}, +}; + +pub struct DocumentationVisitor { + pub globals: Vec<(String, DocsGlobal)>, + pub functions: Vec<(String, DocsFunction)>, + pub params: Vec<(String, DocsParam)>, + pub returns: Vec<(String, DocsReturn)>, + tag_regex: Regex, +} + +impl DocumentationVisitor { + pub fn new() -> Self { + let tag_regex = Regex::new(r#"^@(\w+)\s+(\w+)(.*)$"#).unwrap(); + Self { + globals: vec![], + functions: vec![], + params: vec![], + returns: vec![], + tag_regex, + } + } + + pub fn parse_moonwave_style_tag(&self, line: &str) -> Option { + if self.tag_regex.is_match(line) { + let captures = self.tag_regex.captures(line).unwrap(); + let tag_kind = captures.get(1).unwrap().as_str(); + let tag_name = captures.get(2).unwrap().as_str(); + let tag_contents = captures.get(3).unwrap().as_str(); + Some(DocsTag { + kind: DocsTagKind::parse(tag_kind).unwrap(), + name: tag_name.to_string(), + contents: tag_contents.to_string(), + }) + } else { + None + } + } + + pub fn parse_moonwave_style_comment(&self, comment: &str) -> (String, DocsTagList) { + let lines = comment.lines().map(str::trim).collect::>(); + let indent_len = lines.iter().fold(usize::MAX, |acc, line| { + let first = line.chars().enumerate().find_map(|(idx, ch)| { + if ch.is_alphanumeric() { + Some(idx) + } else { + None + } + }); + if let Some(first_alphanumeric) = first { + if first_alphanumeric > 0 { + acc.min(first_alphanumeric - 1) + } else { + 0 + } + } else { + acc + } + }); + let unindented_lines = lines.iter().map(|line| &line[indent_len..]); + let mut doc_lines = Vec::new(); + let mut doc_tags = DocsTagList::new(); + for line in unindented_lines { + if let Some(tag) = self.parse_moonwave_style_tag(line) { + doc_tags.push(tag); + } else { + doc_lines.push(line); + } + } + (doc_lines.join("\n").trim().to_owned(), doc_tags) + } + + fn extract_moonwave_comment(&mut self, token: &Token) -> Option<(String, DocsTagList)> { + if let TokenType::MultiLineComment { comment, .. } = token.token_type() { + let (doc, tags) = self.parse_moonwave_style_comment(comment); + if doc.is_empty() && tags.is_empty() { + None + } else { + Some((doc, tags)) + } + } else { + None + } + } +} + +impl Visitor for DocumentationVisitor { + fn visit_exported_type_declaration(&mut self, node: &ExportedTypeDeclaration) { + for token in node.export_token().leading_trivia() { + if let Some((doc, mut tags)) = self.extract_moonwave_comment(token) { + if tags.contains(DocsTagKind::Class) { + self.globals.push(( + node.type_declaration().type_name().token().to_string(), + DocsGlobal { + documentation: doc, + ..Default::default() + }, + )); + break; + } + } + } + } + + fn visit_type_field(&mut self, node: &TypeField) { + // Parse out names, moonwave comments from the ast + let mut parsed_data = Vec::new(); + if let TypeFieldKey::Name(name) = node.key() { + for token in name.leading_trivia() { + if let Some((doc, mut tags)) = self.extract_moonwave_comment(token) { + if let Some(within) = tags.find(DocsTagKind::Within).map(ToOwned::to_owned) { + parsed_data.push((within.name, name, doc, tags)); + } + } + } + } + for (global_name, name, doc, tags) in parsed_data { + // Find the global definition, which is guaranteed to + // be visited and parsed before its inner members, and + // add a ref to the found function / member to it + let name = name.token().to_string(); + for (name, global) in &mut self.globals { + if name == &global_name { + global.keys.insert(name.clone(), name.clone()); + } + } + // Look through tags to find and create doc params and returns + let mut param_links = Vec::new(); + let mut return_links = Vec::new(); + for tag in tags { + match tag.kind { + DocsTagKind::Param => { + let idx_string = param_links.len().to_string(); + self.params.push(( + idx_string.clone(), + DocsParam { + global_name: global_name.clone(), + function_name: name.clone(), + documentation: tag.contents.trim().to_owned(), + }, + )); + param_links.push(DocsFunctionParamLink { + name: tag.name.clone(), + documentation: idx_string.clone(), + }); + } + DocsTagKind::Return => { + // NOTE: Returns don't have names but we still parse + // them as such, so we should concat name & contents + let doc = format!("{} {}", tag.name.trim(), tag.contents.trim()); + let idx_string = return_links.len().to_string(); + self.returns.push(( + idx_string.clone(), + DocsReturn { + global_name: global_name.clone(), + function_name: name.clone(), + documentation: doc, + }, + )); + return_links.push(idx_string.clone()); + } + _ => {} + } + } + // Finally, add our complete doc + // function with links into the list + self.functions.push(( + name, + DocsFunction { + global_name, + documentation: doc, + params: param_links, + returns: return_links, + ..Default::default() + }, + )); + } + } +} diff --git a/src/cli/main.rs b/src/cli/main.rs index 3416f98..98e121d 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -1,6 +1,10 @@ #![deny(clippy::all)] #![warn(clippy::cargo, clippy::pedantic)] -#![allow(clippy::needless_pass_by_value, clippy::match_bool)] +#![allow( + clippy::needless_pass_by_value, + clippy::match_bool, + clippy::module_name_repetitions +)] use std::process::ExitCode; @@ -8,6 +12,7 @@ use anyhow::Result; use clap::Parser; mod cli; +mod gen; mod utils; use cli::Cli;