diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d110a834..0bb94c43 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,11 +10,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Run clippy - run: cargo clippy -- -D warnings - - name: Run Rustfmt - run: cargo fmt -- --check + - uses: actions/checkout@v3 - name: Run tests (selene, all features) run: cargo test --release -- --nocapture working-directory: selene @@ -31,9 +27,18 @@ jobs: test_exhaustive_checks: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Install nightly for exhaustive check tests uses: dtolnay/rust-toolchain@nightly - name: Run exhaustive check tests run: cargo check --features force_exhaustive_checks working-directory: selene-lib + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Clippy + run: cargo clippy -- -D warnings + - name: Rustfmt + run: cargo fmt -- --check + if: success() || failure() diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2c2211..cff1241e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,21 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/Kampfkarren/selene/compare/0.25.0...HEAD) ### Added -- Added `table.move`to Lua 5.3 standard library +- Added `table.move` and `math.tointeger` to Lua 5.3 standard library - Added `bit32.*` functions to Lua 5.2 standard library - Added `table.pack`, `rawlen` and `package.config` to Lua 5.2 standard library +- Added new [`empty_loop` lint](https://kampfkarren.github.io/selene/lints/empty_loop.html), which will check for empty loop blocks. +- Added new [`roblox_suspicious_udim2_new` lint](https://kampfkarren.github.io/selene/lints/roblox_suspicious_udim2_new.html), which will warn when you pass in too few number of arguments to `UDim2.new`. +- `roblox_incorrect_roact_usage` now lints for illegal `Name` property +- Added `ignore_pattern` config to `global_usage`, which will ignore any global variables with names that match the pattern +- `roblox_incorrect_roact_usage` now checks for incorrect Roact17's `createElement` usage on variables named `React`. For Roact17 only, `key`, `children`, and `ref` are valid properties to Roblox instances. +- Excludes are now respected for single files. +- Added `no-exclude` cli flag to disable excludes. +- When given in standard library format, additional information now shows up in `incorrect_standard_library_use` missing required parameter errors. +- Added new [`mixed_table` lint](https://kampfkarren.github.io/selene/lints/mixed_table.html), which will warn against mixed tables. +- Added `bit32.byteswap` to Luau standard library +- Added `buffer` library to Luau standard library +- Added `SharedTable` to Roblox standard library ### Changed - Updated internal parser, which includes floor division (`//`), more correct parsing of string interpolation with double braces, and better parsing of `\z` escapes. @@ -13,6 +25,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixed - `string.pack` and `string.unpack` now have proper function signatures in the Lua 5.3 standard library. - Moved `math.log` second argument addition from Lua 5.3 std lib to 5.2 std lib +- `undefined_variable` now correctly errors when defining multiple methods in undefined tables +- Corrected `os.exit` definition in Lua 5.2 standard library +- Fixed `manual_table_clone` incorrectly warning when loop and table are defined at different depths ## [0.25.0](https://github.com/Kampfkarren/selene/releases/tag/0.25.0) - 2023-03-12 ### Added @@ -401,4 +416,4 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Added standard library chaining. This means you can combine two standard libraries by setting `std` in selene.toml to `std1+std2`. You can chain as many as you want. ## [0.1.0](https://github.com/Kampfkarren/selene/releases/tag/0.1.0) - 2019-11-06 -- Initial release +- Initial release \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8c7b4cf0..93740fe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,9 +772,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -863,12 +863,26 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -891,7 +905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", - "ring", + "ring 0.16.20", "sct", "webpki", ] @@ -926,8 +940,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -1071,6 +1085,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.8.0" @@ -1365,6 +1385,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "ureq" version = "2.6.2" @@ -1490,12 +1516,12 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring", - "untrusted", + "ring 0.17.3", + "untrusted 0.9.0", ] [[package]] @@ -1544,43 +1570,109 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_msvc" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_i686_gnu" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_msvc" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_x86_64_gnu" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_msvc" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Dockerfile b/Dockerfile index dea4aae4..5e3abffa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,22 @@ -FROM rust:1.64-bullseye AS selene-builder +ARG RUST_VERSION="1" + +FROM rust:${RUST_VERSION}-bullseye AS selene-builder RUN apt-get update && \ apt-get upgrade -y && \ apt-get install g++ && \ cargo install --branch main --git https://github.com/Kampfkarren/selene selene -FROM rust:1.64-bullseye AS selene-light-builder +FROM rust:${RUST_VERSION}-bullseye AS selene-light-builder RUN apt-get update && \ apt-get upgrade -y && \ apt-get install g++ && \ cargo install --no-default-features --branch main --git https://github.com/Kampfkarren/selene selene -FROM rust:1.64-alpine3.14 AS selene-musl-builder +FROM rust:${RUST_VERSION}-alpine AS selene-musl-builder RUN apk add g++ && \ cargo install --branch main --git https://github.com/Kampfkarren/selene selene -FROM rust:1.64-alpine3.14 AS selene-light-musl-builder +FROM rust:${RUST_VERSION}-alpine AS selene-light-musl-builder RUN apk add g++ && \ cargo install --no-default-features --branch main --git https://github.com/Kampfkarren/selene selene @@ -32,4 +34,4 @@ CMD ["/selene"] FROM bash AS selene-light-musl COPY --from=selene-light-musl-builder /usr/local/cargo/bin/selene / -CMD ["/selene"] +CMD ["/selene"] \ No newline at end of file diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 34ead202..51fcca18 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -19,6 +19,7 @@ - [divide_by_zero](./lints/divide_by_zero.md) - [duplicate_keys](./lints/duplicate_keys.md) - [empty_if](./lints/empty_if.md) + - [empty_loop](./lints/empty_loop.md) - [global_usage](./lints/global_usage.md) - [high_cyclomatic_complexity](./lints/high_cyclomatic_complexity.md) - [if_same_then_else](./lints/if_same_then_else.md) @@ -26,11 +27,13 @@ - [incorrect_standard_library_use](./lints/incorrect_standard_library_use.md) - [manual_table_clone](./lints/manual_table_clone.md) - [mismatched_arg_count](./lints/mismatched_arg_count.md) + - [mixed_table](./lints/mixed_table.md) - [multiple_statements](./lints/multiple_statements.md) - [must_use](./lints/must_use.md) - [parenthese_conditions](./lints/parenthese_conditions.md) - [roblox_incorrect_color3_new_bounds](./lints/roblox_incorrect_color3_new_bounds.md) - [roblox_incorrect_roact_usage](./lints/roblox_incorrect_roact_usage.md) + - [roblox_suspicious_udim2_new](./lints/roblox_suspicious_udim2_new.md) - [shadowing](./lints/shadowing.md) - [suspicious_reverse_loop](./lints/suspicious_reverse_loop.md) - [type_check_inside_call](./lints/type_check_inside_call.md) diff --git a/docs/src/cli/usage.md b/docs/src/cli/usage.md index 648aacf5..25eb5855 100644 --- a/docs/src/cli/usage.md +++ b/docs/src/cli/usage.md @@ -8,6 +8,7 @@ USAGE: FLAGS: --allow-warnings Pass when only warnings occur + --no-exclude Ignore excludes defined in config -h, --help Prints help information -n, --no-summary Suppress summary information -q, --quiet Display only the necessary information. Equivalent to --display-style="quiet" @@ -81,4 +82,4 @@ Specifies the number of threads for selene to use. Defaults to however many core **--pattern** *pattern* -A [glob](https://en.wikipedia.org/wiki/Glob_(programming)) to match what files selene should check for. For example, if you only wanted to check files that end with `.spec.lua`, you would input `--pattern **/*.spec.lua`. Defaults to `**/*.lua`, meaning "any lua file", or `**/*.lua` and `**/*.luau` with the roblox feature flag, meaning "any lua/luau file". +A [glob](https://en.wikipedia.org/wiki/Glob_(programming)) to match what files selene should check for. For example, if you only wanted to check files that end with `.spec.lua`, you would input `--pattern **/*.spec.lua`. Defaults to `**/*.lua`, meaning "any lua file", or `**/*.lua` and `**/*.luau` with the roblox feature flag, meaning "any lua/luau file". \ No newline at end of file diff --git a/docs/src/contributing.md b/docs/src/contributing.md index 3870565a..a8a1f52b 100644 --- a/docs/src/contributing.md +++ b/docs/src/contributing.md @@ -130,7 +130,7 @@ The first `"cool_lint"` is the name of the folder we created. The second `"cool_ Now, just run `cargo test`, and a `.stderr` file will be automatically generated! You can manipulate it however you see fit as well as modifying your lint, and so long as the file is there, selene will make sure that its accurate. -Optionally, you can add a `.std.toml` with the same name as the test next to the lua file, where you can specify a custom [standard library](./usage/std.html) to use. If you do not, the Lua 5.1 standard library will be used. +Optionally, you can add a `.std.toml` with the same name as the test next to the lua file, where you can specify a custom [standard library](./usage/std.md) to use. If you do not, the Lua 5.1 standard library will be used. ### Documenting it diff --git a/docs/src/lints/empty_loop.md b/docs/src/lints/empty_loop.md new file mode 100644 index 00000000..3abffa14 --- /dev/null +++ b/docs/src/lints/empty_loop.md @@ -0,0 +1,20 @@ +# empty_loop +## What it does +Checks for empty loop blocks. + +## Why this is bad +You most likely forgot to write code in there or commented it out without commenting out the loop statement itself. + +## Configuration +`comments_count` (default: `false`) - A bool that determines whether or not if statements with exclusively comments are empty. + +## Example +```lua +-- Counts as an empty loop +for _ in {} do +end + +for _ in {} do + -- If comments_count is true, this will not count as empty. +end +``` diff --git a/docs/src/lints/global_usage.md b/docs/src/lints/global_usage.md index 09e51073..8f59b2bd 100644 --- a/docs/src/lints/global_usage.md +++ b/docs/src/lints/global_usage.md @@ -5,6 +5,9 @@ Prohibits use of `_G`. ## Why this is bad `_G` is global mutable state, which is heavily regarded as harmful. You should instead refactor your code to be more modular in nature. +## Configuration +`ignore_pattern` - A [regular expression](https://en.wikipedia.org/wiki/Regular_expression) for variables that are allowed to be global variables. The default disallows all global variables regardless of their name. + ## Remarks If you are using the Roblox standard library, use of `shared` is prohibited under this lint. diff --git a/docs/src/lints/manual_table_clone.md b/docs/src/lints/manual_table_clone.md index 52559832..0c88d5da 100644 --- a/docs/src/lints/manual_table_clone.md +++ b/docs/src/lints/manual_table_clone.md @@ -27,6 +27,7 @@ Very little outside this exact pattern is matched. This is the list of circumsta - Any usage of the output variable in between the definition and the loop (as determined by position in code). - If the input variable is not a plain locally initialized variable. For example, `self.state[key] = value` will not lint. - If the input variable is not defined as a completely empty table. +- If the loop and input variable are defined at different depths. --- diff --git a/docs/src/lints/mixed_table.md b/docs/src/lints/mixed_table.md new file mode 100644 index 00000000..71e633d9 --- /dev/null +++ b/docs/src/lints/mixed_table.md @@ -0,0 +1,14 @@ +# mixed_table +## What it does +Checks for mixed tables (tables that act as both an array and dictionary). + +## Why this is bad +Mixed tables harms readability and are prone to bugs. There is almost always a better alternative. + +## Example +```lua +local foo = { + "array field", + bar = "dictionary field", +} +``` diff --git a/docs/src/lints/roblox_incorrect_roact_usage.md b/docs/src/lints/roblox_incorrect_roact_usage.md index eddc6757..0b6cb77a 100644 --- a/docs/src/lints/roblox_incorrect_roact_usage.md +++ b/docs/src/lints/roblox_incorrect_roact_usage.md @@ -1,14 +1,22 @@ # roblox_incorrect_roact_usage ## What it does -Checks for valid uses of Roact.createElement. Verifies that class name given is valid and that the properties passed for it are valid for that class. +Checks for valid uses of createElement. Verifies that class name given is valid and that the properties passed for it are valid for that class. ## Why this is bad -This is guaranteed to fail once it is rendered. Furthermore, the createElement itself will not error--only once its mounted will it error. +This is guaranteed to fail once it is rendered. Furthermore, the createElement itself will not error--only once it's mounted will it error. ## Example ```lua +-- Using Roact17 +React.createElement("Frame", { + key = "Valid property for React", +}) + +-- Using legacy Roact Roact.createElement("Frame", { + key = "Invalid property for Roact", ThisPropertyDoesntExist = true, + Name = "This property should not be passed in", [Roact.Event.ThisEventDoesntExist] = function() end, }) @@ -19,9 +27,11 @@ Roact.createElement("BadClass", {}) ## Remarks This lint is naive and makes several assumptions about the way you write your code. The assumptions are based on idiomatic Roact. -1. It assumes you are either calling `Roact.createElement` directly or creating a local variable that's assigned to `Roact.createElement`. +1. It assumes you are either calling `createElement` directly or creating a local variable that's assigned to `[Roact/React].createElement`. 2. It assumes if you are using a local variable, you're not reassigning it. -3. It assumes Roact is defined. [`undefined_variable`](./undefined_variable.md) will still lint, however. +3. It assumes either Roact or React is defined. [`undefined_variable`](./undefined_variable.md) will still lint, however. + +This lint assumes legacy Roact if the variable name is `Roact` and Roact17 if the variable name is named `React`. This lint does not verify if the value you are giving is correct, so `Text = UDim2.new()` will be treated as correct. This lint, right now, only checks property and class names. diff --git a/docs/src/lints/roblox_suspicious_udim2_new.md b/docs/src/lints/roblox_suspicious_udim2_new.md new file mode 100644 index 00000000..ef51da53 --- /dev/null +++ b/docs/src/lints/roblox_suspicious_udim2_new.md @@ -0,0 +1,19 @@ +# roblox_suspicious_udim2_new +## What it does +Checks for too little arguments passed to `UDim2.new()`. + +## Why this is bad +Passing in an incorrect number of arguments can indicate that the user meant to use `UDim2.fromScale` or `UDim2.fromOffset`. +Even if the user really only needed to pass in a fewer number of arguments to `UDim2.new`, this lowers readability +as it calls into question whether it's a bug or if the user truly meant to use `UDim2.new`. + +## Example +```lua +UDim2.new(1, 1) -- error, UDim2.new takes 4 numbers, but 2 were provided. +``` + +## Remarks +This lint is only active if you are using the Roblox standard library. + +This lint does not warn if passing in exactly 2 arguments and none of those are number literals to prevent false positives +with `UDim2.new(UDim.new(a, b), UDim.new(c, d))` diff --git a/docs/src/roblox.md b/docs/src/roblox.md index b694ec04..77ed0709 100644 --- a/docs/src/roblox.md +++ b/docs/src/roblox.md @@ -2,7 +2,7 @@ selene is built with Roblox development in mind, and has special features for Roblox developers. -If you try to run selene on a Roblox codebase, you'll get a bunch of errors saying things such as "`game` is not defined". This is because these are Roblox specific globals that selene does not know about. You'll need to install the Roblox [standard library](./usage/configuration) in order to fix these issues, as well as get Roblox specific lints. +If you try to run selene on a Roblox codebase, you'll get a bunch of errors saying things such as "`game` is not defined". This is because these are Roblox specific globals that selene does not know about. You'll need to install the Roblox [standard library](./usage/configuration.md) in order to fix these issues, as well as get Roblox specific lints. ## Installation diff --git a/docs/src/usage/std.md b/docs/src/usage/std.md index 19e97f3c..cc221491 100644 --- a/docs/src/usage/std.md +++ b/docs/src/usage/std.md @@ -150,7 +150,7 @@ globals: - type: table - type: number deprecated: - message: "`table.getn` has been superceded by #." + message: "`table.getn` has been superseded by #." replace: - "#%1" ``` diff --git a/selene-lib/default_std/lua51.yml b/selene-lib/default_std/lua51.yml index 387aacea..b0098ac1 100644 --- a/selene-lib/default_std/lua51.yml +++ b/selene-lib/default_std/lua51.yml @@ -593,7 +593,7 @@ globals: - type: table - type: number deprecated: - message: "`table.getn` has been superceded by #." + message: "`table.getn` has been superseded by #." replace: - "#%1" must_use: true diff --git a/selene-lib/default_std/lua52.yml b/selene-lib/default_std/lua52.yml index d247070a..8fae55e8 100644 --- a/selene-lib/default_std/lua52.yml +++ b/selene-lib/default_std/lua52.yml @@ -68,6 +68,12 @@ globals: - type: number - required: false type: number + os.exit: + args: + - required: false + type: number + - required: false + type: bool package.config: property: read-only rawlen: diff --git a/selene-lib/default_std/lua53.yml b/selene-lib/default_std/lua53.yml index bfed4478..528dd2cd 100644 --- a/selene-lib/default_std/lua53.yml +++ b/selene-lib/default_std/lua53.yml @@ -1,6 +1,10 @@ --- base: lua52 globals: + math.tointeger: + args: + - type: number + must_use: true string.pack: args: - type: string diff --git a/selene-lib/default_std/luau.yml b/selene-lib/default_std/luau.yml index 8d41cb45..0243c110 100644 --- a/selene-lib/default_std/luau.yml +++ b/selene-lib/default_std/luau.yml @@ -29,6 +29,10 @@ globals: args: - type: "..." must_use: true + bit32.byteswap: + args: + - type: number + must_use: true bit32.countlz: args: - type: number @@ -72,6 +76,154 @@ globals: - type: number - type: number must_use: true + buffer.copy: + args: + - type: + display: buffer + - type: number + - type: + display: buffer + - required: false + type: number + - required: false + type: number + buffer.create: + args: + - type: number + must_use: true + buffer.fill: + args: + - type: + display: buffer + - type: number + - type: number + - required: false + type: number + buffer.fromstring: + args: + - type: string + must_use: true + buffer.len: + args: + - type: + display: buffer + must_use: true + buffer.readf32: + args: + - type: + display: buffer + - type: number + must_use: true + buffer.readf64: + args: + - type: + display: buffer + - type: number + must_use: true + buffer.readi8: + args: + - type: + display: buffer + - type: number + must_use: true + buffer.readi16: + args: + - type: + display: buffer + - type: number + must_use: true + buffer.readi32: + args: + - type: + display: buffer + - type: number + must_use: true + buffer.readstring: + args: + - type: + display: buffer + - type: number + - type: number + must_use: true + buffer.readu8: + args: + - type: + display: buffer + - type: number + must_use: true + buffer.readu16: + args: + - type: + display: buffer + - type: number + must_use: true + buffer.readu32: + args: + - type: + display: buffer + - type: number + must_use: true + buffer.tostring: + args: + - type: + display: buffer + must_use: true + buffer.writef32: + args: + - type: + display: buffer + - type: number + - type: number + buffer.writef64: + args: + - type: + display: buffer + - type: number + - type: number + buffer.writei8: + args: + - type: + display: buffer + - type: number + - type: number + buffer.writei16: + args: + - type: + display: buffer + - type: number + - type: number + buffer.writei32: + args: + - type: + display: buffer + - type: number + - type: number + buffer.writestring: + args: + - type: + display: buffer + - type: number + - type: string + - required: false + type: number + buffer.writeu8: + args: + - type: + display: buffer + - type: number + - type: number + buffer.writeu16: + args: + - type: + display: buffer + - type: number + - type: number + buffer.writeu32: + args: + - type: + display: buffer + - type: number + - type: number collectgarbage: args: - type: diff --git a/selene-lib/default_std/roblox_base.yml b/selene-lib/default_std/roblox_base.yml index 07e46fa5..375372dd 100644 --- a/selene-lib/default_std/roblox_base.yml +++ b/selene-lib/default_std/roblox_base.yml @@ -533,6 +533,51 @@ globals: require: args: - type: number + SharedTable.clear: + args: + - type: + display: SharedTable + SharedTable.clone: + args: + - type: + display: SharedTable + - required: false + type: bool + must_use: true + SharedTable.cloneAndFreeze: + args: + - type: + display: SharedTable + - required: false + type: bool + must_use: true + SharedTable.increment: + args: + - type: + display: SharedTable + - type: any + - type: number + SharedTable.isFrozen: + args: + - type: + display: SharedTable + must_use: true + SharedTable.new: + args: + - required: false + type: table + must_use: true + SharedTable.size: + args: + - type: + display: SharedTable + must_use: true + SharedTable.update: + args: + - type: + display: SharedTable + - type: any + - type: function settings: args: [] shared: @@ -611,7 +656,7 @@ structs: method: true connect: deprecated: - message: "lowercase methods have been superceded by uppercase ones" + message: "lowercase methods have been superseded by uppercase ones" replace: - "Connect(%1)" args: @@ -626,7 +671,7 @@ structs: method: true wait: deprecated: - message: "lowercase methods have been superceded by uppercase ones" + message: "lowercase methods have been superseded by uppercase ones" replace: - "Wait(%1)" args: diff --git a/selene-lib/src/ast_util/loop_tracker.rs b/selene-lib/src/ast_util/loop_tracker.rs index de35f4b3..dde70c92 100644 --- a/selene-lib/src/ast_util/loop_tracker.rs +++ b/selene-lib/src/ast_util/loop_tracker.rs @@ -1,3 +1,6 @@ +// Remove this once loop_tracker is used again +#![allow(dead_code)] + use full_moon::{ast, node::Node, visitors::Visitor}; #[derive(Debug, Clone, Copy)] diff --git a/selene-lib/src/ast_util/mod.rs b/selene-lib/src/ast_util/mod.rs index b254fe9b..98690f26 100644 --- a/selene-lib/src/ast_util/mod.rs +++ b/selene-lib/src/ast_util/mod.rs @@ -16,7 +16,6 @@ mod strip_parentheses; pub mod visit_nodes; pub use extract_static_token::extract_static_token; -pub use loop_tracker::LoopTracker; pub use purge_trivia::purge_trivia; pub use side_effects::HasSideEffects; pub use strip_parentheses::strip_parentheses; diff --git a/selene-lib/src/ast_util/scopes.rs b/selene-lib/src/ast_util/scopes.rs index 58fa3d79..ed817d97 100644 --- a/selene-lib/src/ast_util/scopes.rs +++ b/selene-lib/src/ast_util/scopes.rs @@ -934,7 +934,7 @@ impl Visitor for ScopeVisitor { let mut names = name.names().iter(); let base = names.next().unwrap(); - let is_longer_expression = names.next().is_some(); + let is_longer_expression = names.next().is_some() || name.method_colon().is_some(); if is_longer_expression { self.write_name_with( diff --git a/selene-lib/src/lib.rs b/selene-lib/src/lib.rs index 299771df..77bb09c3 100644 --- a/selene-lib/src/lib.rs +++ b/selene-lib/src/lib.rs @@ -303,6 +303,7 @@ use_lints! { divide_by_zero: lints::divide_by_zero::DivideByZeroLint, duplicate_keys: lints::duplicate_keys::DuplicateKeysLint, empty_if: lints::empty_if::EmptyIfLint, + empty_loop: lints::empty_loop::EmptyLoopLint, global_usage: lints::global_usage::GlobalLint, high_cyclomatic_complexity: lints::high_cyclomatic_complexity::HighCyclomaticComplexityLint, if_same_then_else: lints::if_same_then_else::IfSameThenElseLint, @@ -311,6 +312,7 @@ use_lints! { invalid_lint_filter: lints::invalid_lint_filter::InvalidLintFilterLint, manual_table_clone: lints::manual_table_clone::ManualTableCloneLint, mismatched_arg_count: lints::mismatched_arg_count::MismatchedArgCountLint, + mixed_table: lints::mixed_table::MixedTableLint, multiple_statements: lints::multiple_statements::MultipleStatementsLint, must_use: lints::must_use::MustUseLint, parenthese_conditions: lints::parenthese_conditions::ParentheseConditionsLint, @@ -326,5 +328,6 @@ use_lints! { { roblox_incorrect_color3_new_bounds: lints::roblox_incorrect_color3_new_bounds::Color3BoundsLint, roblox_incorrect_roact_usage: lints::roblox_incorrect_roact_usage::IncorrectRoactUsageLint, + roblox_suspicious_udim2_new: lints::roblox_suspicious_udim2_new::SuspiciousUDim2NewLint, }, } diff --git a/selene-lib/src/lints.rs b/selene-lib/src/lints.rs index 10055f91..fa42553c 100644 --- a/selene-lib/src/lints.rs +++ b/selene-lib/src/lints.rs @@ -15,6 +15,7 @@ pub mod deprecated; pub mod divide_by_zero; pub mod duplicate_keys; pub mod empty_if; +pub mod empty_loop; pub mod global_usage; pub mod high_cyclomatic_complexity; pub mod if_same_then_else; @@ -22,6 +23,7 @@ pub mod ifs_same_cond; pub mod invalid_lint_filter; pub mod manual_table_clone; pub mod mismatched_arg_count; +pub mod mixed_table; pub mod multiple_statements; pub mod must_use; pub mod parenthese_conditions; @@ -40,6 +42,9 @@ pub mod roblox_incorrect_color3_new_bounds; #[cfg(feature = "roblox")] pub mod roblox_incorrect_roact_usage; +#[cfg(feature = "roblox")] +pub mod roblox_suspicious_udim2_new; + #[cfg(test)] mod test_util; diff --git a/selene-lib/src/lints/empty_loop.rs b/selene-lib/src/lints/empty_loop.rs new file mode 100644 index 00000000..df35d62e --- /dev/null +++ b/selene-lib/src/lints/empty_loop.rs @@ -0,0 +1,137 @@ +use super::*; +use crate::ast_util::range; +use std::convert::Infallible; + +use full_moon::{ + ast::{self, Ast}, + tokenizer::{Token, TokenKind}, + visitors::Visitor, +}; +use serde::Deserialize; + +#[derive(Clone, Copy, Default, Deserialize)] +#[serde(default)] +pub struct EmptyLoopLintConfig { + comments_count: bool, +} + +pub struct EmptyLoopLint { + config: EmptyLoopLintConfig, +} + +impl Lint for EmptyLoopLint { + type Config = EmptyLoopLintConfig; + type Error = Infallible; + + const SEVERITY: Severity = Severity::Warning; + const LINT_TYPE: LintType = LintType::Style; + + fn new(config: Self::Config) -> Result { + Ok(EmptyLoopLint { config }) + } + + fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec { + let mut visitor = EmptyLoopVisitor { + comment_positions: Vec::new(), + positions: Vec::new(), + }; + + visitor.visit_ast(ast); + + let comment_positions = visitor.comment_positions.clone(); + + visitor + .positions + .into_iter() + .filter(|position| { + // OPTIMIZE: This is O(n^2), can we optimize this? + if self.config.comments_count { + !comment_positions.iter().any(|comment_position| { + position.0 <= *comment_position && position.1 >= *comment_position + }) + } else { + true + } + }) + .map(|position| { + Diagnostic::new( + "empty_loop", + "empty loop block".to_owned(), + Label::new(position), + ) + }) + .collect() + } +} + +struct EmptyLoopVisitor { + comment_positions: Vec, + positions: Vec<(u32, u32)>, +} + +fn block_is_empty(block: &ast::Block) -> bool { + block.last_stmt().is_none() && block.stmts().next().is_none() +} + +impl Visitor for EmptyLoopVisitor { + fn visit_generic_for(&mut self, node: &ast::GenericFor) { + if block_is_empty(node.block()) { + self.positions.push(range(node)); + } + } + + fn visit_numeric_for(&mut self, node: &ast::NumericFor) { + if block_is_empty(node.block()) { + self.positions.push(range(node)); + } + } + + fn visit_while(&mut self, node: &ast::While) { + if block_is_empty(node.block()) { + self.positions.push(range(node)); + } + } + + fn visit_repeat(&mut self, node: &ast::Repeat) { + if block_is_empty(node.block()) { + self.positions.push(range(node)); + } + } + + fn visit_token(&mut self, token: &Token) { + match token.token_kind() { + TokenKind::MultiLineComment | TokenKind::SingleLineComment => { + self.comment_positions + .push(Token::end_position(token).bytes() as u32); + } + + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::{super::test_util::test_lint, *}; + + #[test] + fn test_empty_loop() { + test_lint( + EmptyLoopLint::new(EmptyLoopLintConfig::default()).unwrap(), + "empty_loop", + "empty_loop", + ); + } + + #[test] + fn test_empty_loop_comments() { + test_lint( + EmptyLoopLint::new(EmptyLoopLintConfig { + comments_count: true, + }) + .unwrap(), + "empty_loop", + "empty_loop_comments", + ); + } +} diff --git a/selene-lib/src/lints/global_usage.rs b/selene-lib/src/lints/global_usage.rs index ee8177f4..ee170c2b 100644 --- a/selene-lib/src/lints/global_usage.rs +++ b/selene-lib/src/lints/global_usage.rs @@ -1,23 +1,38 @@ use super::*; -use std::{collections::HashSet, convert::Infallible}; +use std::collections::HashSet; use full_moon::ast::Ast; +use regex::Regex; +use serde::Deserialize; fn is_global(name: &str, roblox: bool) -> bool { (roblox && name == "shared") || name == "_G" } -pub struct GlobalLint; +#[derive(Clone, Default, Deserialize)] +#[serde(default)] +pub struct GlobalConfig { + ignore_pattern: Option, +} + +pub struct GlobalLint { + ignore_pattern: Option, +} impl Lint for GlobalLint { - type Config = (); - type Error = Infallible; + type Config = GlobalConfig; + type Error = regex::Error; const SEVERITY: Severity = Severity::Warning; const LINT_TYPE: LintType = LintType::Complexity; - fn new(_: Self::Config) -> Result { - Ok(GlobalLint) + fn new(config: Self::Config) -> Result { + Ok(GlobalLint { + ignore_pattern: config + .ignore_pattern + .map(|ignore_pattern| Regex::new(&ignore_pattern)) + .transpose()?, + }) } fn pass(&self, _: &Ast, context: &Context, ast_context: &AstContext) -> Vec { @@ -30,7 +45,25 @@ impl Lint for GlobalLint { .filter(|(_, reference)| { if !checked.contains(&reference.identifier) { checked.insert(reference.identifier); - is_global(&reference.name, context.is_roblox()) && reference.resolved.is_none() + + let matches_ignore_pattern = match &self.ignore_pattern { + Some(ignore_pattern) => match reference + .indexing + .as_ref() + .and_then(|indexing| indexing.first()) + .and_then(|index_entry| index_entry.static_name.as_ref()) + { + // Trim whitespace at the end as `_G.a = 1` yields `a ` + Some(name) => ignore_pattern + .is_match(name.to_string().trim_end_matches(char::is_whitespace)), + None => false, + }, + None => false, + }; + + is_global(&reference.name, context.is_roblox()) + && !matches_ignore_pattern + && reference.resolved.is_none() } else { false } @@ -55,6 +88,30 @@ mod tests { #[test] fn test_global_usage() { - test_lint(GlobalLint::new(()).unwrap(), "global_usage", "global_usage"); + test_lint( + GlobalLint::new(GlobalConfig::default()).unwrap(), + "global_usage", + "global_usage", + ); + } + + #[test] + fn test_invalid_regex() { + assert!(GlobalLint::new(GlobalConfig { + ignore_pattern: Some("(".to_owned()), + }) + .is_err()); + } + + #[test] + fn test_global_usage_ignore() { + test_lint( + GlobalLint::new(GlobalConfig { + ignore_pattern: Some("^_.*_$".to_owned()), + }) + .unwrap(), + "global_usage", + "global_usage_ignore", + ); } } diff --git a/selene-lib/src/lints/high_cyclomatic_complexity.rs b/selene-lib/src/lints/high_cyclomatic_complexity.rs index c45f55c3..69e9ecbd 100644 --- a/selene-lib/src/lints/high_cyclomatic_complexity.rs +++ b/selene-lib/src/lints/high_cyclomatic_complexity.rs @@ -131,10 +131,9 @@ fn count_suffix_complexity(suffix: &ast::Suffix, starting_complexity: u16) -> u1 ast::Suffix::Index(ast::Index::Brackets { expression, .. }) => { complexity = count_expression_complexity(expression, complexity) } - #[cfg_attr( - feature = "force_exhaustive_checks", - deny(non_exhaustive_omitted_patterns) - )] + ast::Suffix::Index(ast::Index::Dot { .. }) => { + // Dot indexing doesn't contribute to complexity + } ast::Suffix::Call(call) => match call { ast::Call::AnonymousCall(arguments) => { complexity = count_arguments_complexity(arguments, complexity) diff --git a/selene-lib/src/lints/if_same_then_else.rs b/selene-lib/src/lints/if_same_then_else.rs index 502e1bbf..c8874b9c 100644 --- a/selene-lib/src/lints/if_same_then_else.rs +++ b/selene-lib/src/lints/if_same_then_else.rs @@ -55,8 +55,8 @@ impl Visitor for IfSameThenElseVisitor { fn visit_if(&mut self, if_block: &ast::If) { let else_ifs = if_block .else_if() - .map(|else_ifs| else_ifs.iter().collect()) - .unwrap_or_else(Vec::new); + .map(|else_ifs| else_ifs.iter().collect::>()) + .unwrap_or_default(); let mut blocks = Vec::with_capacity(2 + else_ifs.len()); blocks.push(if_block.block()); diff --git a/selene-lib/src/lints/manual_table_clone.rs b/selene-lib/src/lints/manual_table_clone.rs index b96c3c91..c5e7c1a6 100644 --- a/selene-lib/src/lints/manual_table_clone.rs +++ b/selene-lib/src/lints/manual_table_clone.rs @@ -3,12 +3,10 @@ use full_moon::{ visitors::Visitor, }; -use crate::ast_util::{ - expression_to_ident, range, scopes::AssignedValue, strip_parentheses, LoopTracker, -}; +use crate::ast_util::{expression_to_ident, range, scopes::AssignedValue, strip_parentheses}; use super::*; -use std::convert::Infallible; +use std::{collections::HashSet, convert::Infallible}; pub struct ManualTableCloneLint; @@ -34,9 +32,9 @@ impl Lint for ManualTableCloneLint { let mut visitor = ManualTableCloneVisitor { matches: Vec::new(), - loop_tracker: LoopTracker::new(ast), scope_manager: &ast_context.scope_manager, - stmt_begins: Vec::new(), + completed_stmt_begins: Vec::new(), + inside_stmt_begins: HashSet::new(), }; visitor.visit_ast(ast); @@ -92,9 +90,9 @@ impl ManualTableCloneMatch { struct ManualTableCloneVisitor<'ast> { matches: Vec, - loop_tracker: LoopTracker, scope_manager: &'ast ScopeManager, - stmt_begins: Vec, + completed_stmt_begins: Vec, + inside_stmt_begins: HashSet, } #[derive(Debug)] @@ -208,7 +206,7 @@ impl ManualTableCloneVisitor<'_> { ) -> bool { debug_assert!(assigment_start > definition_end); - for &stmt_begin in self.stmt_begins.iter() { + for &stmt_begin in self.completed_stmt_begins.iter() { if stmt_begin > definition_end { return true; } else if stmt_begin > assigment_start { @@ -218,6 +216,13 @@ impl ManualTableCloneVisitor<'_> { false } + + fn get_depth_at_byte(&self, byte: usize) -> usize { + self.inside_stmt_begins + .iter() + .filter(|&&start| start < byte) + .count() + } } fn has_filter_comment(for_loop: &ast::GenericFor) -> bool { @@ -368,9 +373,7 @@ impl Visitor for ManualTableCloneVisitor<'_> { let (position_start, position_end) = range(node); - if self.loop_tracker.depth_at_byte(position_start) - != self.loop_tracker.depth_at_byte(*definition_start) - { + if self.get_depth_at_byte(*definition_start) != self.get_depth_at_byte(position_start) { return; } @@ -395,8 +398,13 @@ impl Visitor for ManualTableCloneVisitor<'_> { }); } + fn visit_stmt(&mut self, stmt: &ast::Stmt) { + self.inside_stmt_begins.insert(range(stmt).0); + } + fn visit_stmt_end(&mut self, stmt: &ast::Stmt) { - self.stmt_begins.push(range(stmt).0); + self.completed_stmt_begins.push(range(stmt).0); + self.inside_stmt_begins.remove(&range(stmt).0); } } diff --git a/selene-lib/src/lints/mixed_table.rs b/selene-lib/src/lints/mixed_table.rs new file mode 100644 index 00000000..4a588abf --- /dev/null +++ b/selene-lib/src/lints/mixed_table.rs @@ -0,0 +1,92 @@ +use super::*; +use crate::ast_util::range; +use std::convert::Infallible; + +use full_moon::{ + ast::{self, Ast}, + visitors::Visitor, +}; + +pub struct MixedTableLint; + +impl Lint for MixedTableLint { + type Config = (); + type Error = Infallible; + + const SEVERITY: Severity = Severity::Warning; + const LINT_TYPE: LintType = LintType::Correctness; + + fn new(_: Self::Config) -> Result { + Ok(MixedTableLint) + } + + fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec { + let mut visitor = MixedTableVisitor::default(); + + visitor.visit_ast(ast); + + let mut diagnostics = Vec::new(); + + for mixed_table in visitor.mixed_tables { + diagnostics.push(Diagnostic::new_complete( + "mixed_table", + "mixed tables are not allowed".to_owned(), + Label::new(mixed_table.range), + vec!["help: change this table to either an array or dictionary".to_owned()], + Vec::new(), + )); + } + + diagnostics + } +} + +#[derive(Default)] +struct MixedTableVisitor { + mixed_tables: Vec, +} + +struct MixedTable { + range: (usize, usize), +} + +impl Visitor for MixedTableVisitor { + fn visit_table_constructor(&mut self, node: &ast::TableConstructor) { + let mut last_key_field_starting_range = 0; + let mut last_no_key_field_starting_range = 0; + + for field in node.fields() { + if let ast::Field::NoKey(_) = field { + if last_key_field_starting_range > 0 { + self.mixed_tables.push(MixedTable { + range: (last_key_field_starting_range, range(field).1), + }); + return; + } + last_no_key_field_starting_range = range(field).0; + } else { + if last_no_key_field_starting_range > 0 { + self.mixed_tables.push(MixedTable { + range: (last_no_key_field_starting_range, range(field).1), + }); + return; + } + last_key_field_starting_range = range(field).0; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{super::test_util::test_lint, *}; + + #[test] + fn test_mixed_table() { + test_lint( + MixedTableLint::new(()).unwrap(), + "mixed_table", + "mixed_table", + ); + } +} diff --git a/selene-lib/src/lints/roblox_incorrect_roact_usage.rs b/selene-lib/src/lints/roblox_incorrect_roact_usage.rs index 891f0816..f82e600f 100644 --- a/selene-lib/src/lints/roblox_incorrect_roact_usage.rs +++ b/selene-lib/src/lints/roblox_incorrect_roact_usage.rs @@ -4,7 +4,7 @@ use crate::{ standard_library::RobloxClass, }; use std::{ - collections::{BTreeMap, HashSet}, + collections::{BTreeMap, HashMap}, convert::Infallible, }; @@ -17,6 +17,38 @@ use if_chain::if_chain; pub struct IncorrectRoactUsageLint; +// Assumes string includes quotes at start and end +fn is_lua_valid_table_key_identifier(string: &String) -> bool { + // Valid identifier cannot start with numbers + let first_char = string.chars().nth(1).unwrap(); + if !first_char.is_alphabetic() && first_char != '_' { + return false; + } + + string + .chars() + .skip(1) + .take(string.len() - 2) + .all(|c| c.is_alphanumeric() || c == '_') +} + +fn get_lua_table_key_format(expression: &ast::Expression) -> String { + match expression { + ast::Expression::Value { value, .. } => match &**value { + ast::Value::String(token) => { + let string = token.to_string(); + if is_lua_valid_table_key_identifier(&string) { + string[1..string.len() - 1].to_string() + } else { + format!("[{}]", string) + } + } + _ => format!("[{}]", expression), + }, + _ => format!("[{}]", expression), + } +} + impl Lint for IncorrectRoactUsageLint { type Config = (); type Error = Infallible; @@ -41,7 +73,7 @@ impl Lint for IncorrectRoactUsageLint { } let mut visitor = IncorrectRoactUsageVisitor { - definitions_of_create_element: HashSet::new(), + definitions_of_create_element: HashMap::new(), invalid_events: Vec::new(), invalid_properties: Vec::new(), unknown_class: Vec::new(), @@ -65,14 +97,34 @@ impl Lint for IncorrectRoactUsageLint { } for invalid_property in visitor.invalid_properties { - diagnostics.push(Diagnostic::new( - "roblox_incorrect_roact_usage", - format!( - "`{}` is not a property of `{}`", - invalid_property.property_name, invalid_property.class_name - ), - Label::new(invalid_property.range), - )); + match invalid_property.property_name.as_str() { + "Name" => { + diagnostics.push(Diagnostic::new_complete( + "roblox_incorrect_roact_usage", + format!( + "`{}` is assigned through the element's key for Roblox instances", + invalid_property.property_name + ), + Label::new(invalid_property.range), + vec![format!( + "try: {} = {}(...)", + get_lua_table_key_format(&invalid_property.property_value), + invalid_property.create_element_expression, + )], + Vec::new(), + )); + } + _ => { + diagnostics.push(Diagnostic::new( + "roblox_incorrect_roact_usage", + format!( + "`{}` is not a property of `{}`", + invalid_property.property_name, invalid_property.class_name + ), + Label::new(invalid_property.range), + )); + } + } } for unknown_class in visitor.unknown_class { @@ -87,23 +139,37 @@ impl Lint for IncorrectRoactUsageLint { } } -fn is_roact_create_element(prefix: &ast::Prefix, suffixes: &[&ast::Suffix]) -> bool { +fn is_roact_or_react_create_element( + prefix: &ast::Prefix, + suffixes: &[&ast::Suffix], +) -> Option { if_chain! { if let ast::Prefix::Name(prefix_token) = prefix; - if prefix_token.token().to_string() == "Roact"; + if let Some(library_name) = match prefix_token.token().to_string().as_str() { + "Roact" => Some(LibraryName::Roact), + "React" => Some(LibraryName::React), + _ => None, + }; if suffixes.len() == 1; if let ast::Suffix::Index(ast::Index::Dot { name, .. }) = suffixes[0]; + if name.token().to_string() == "createElement"; then { - name.token().to_string() == "createElement" + Some(library_name) } else { - false + None } } } +#[derive(Debug, PartialEq, Clone, Copy)] +enum LibraryName { + Roact, + React, +} + #[derive(Debug)] struct IncorrectRoactUsageVisitor<'a> { - definitions_of_create_element: HashSet, + definitions_of_create_element: HashMap, invalid_events: Vec, invalid_properties: Vec, unknown_class: Vec, @@ -122,6 +188,8 @@ struct InvalidEvent { struct InvalidProperty { class_name: String, property_name: String, + property_value: ast::Expression, + create_element_expression: String, range: (usize, usize), } @@ -160,29 +228,36 @@ impl<'a> Visitor for IncorrectRoactUsageVisitor<'a> { let mut suffixes = call.suffixes().collect::>(); let call_suffix = suffixes.pop(); - let mut check = false; + let mut library_name = None; + let mut create_element_expression = String::new(); if suffixes.is_empty() { // Call is foo(), not foo.bar() // Check if foo is a variable for Roact.createElement if let ast::Prefix::Name(name) = call.prefix() { - if self + if let Some(react_name) = self .definitions_of_create_element - .contains(&name.token().to_string()) + .get(&name.token().to_string()) { - check = true; + library_name = Some(*react_name); + create_element_expression = name.token().to_string(); } } } else if suffixes.len() == 1 { // Call is foo.bar() // Check if foo.bar is Roact.createElement - check = is_roact_create_element(call.prefix(), &suffixes); - } + library_name = is_roact_or_react_create_element(call.prefix(), &suffixes); - if !check { - return; + if let ast::Prefix::Name(name) = call.prefix() { + create_element_expression = format!("{}{}", name.token(), suffixes[0]); + } } + let react_name = match library_name { + Some(name) => name, + None => return, + }; + let ((name, class), arguments) = if_chain! { if let Some(ast::Suffix::Call(ast::Call::AnonymousCall( ast::FunctionArgs::Parentheses { arguments, .. } @@ -207,12 +282,23 @@ impl<'a> Visitor for IncorrectRoactUsageVisitor<'a> { for field in arguments.fields() { match field { - ast::Field::NameKey { key, .. } => { + ast::Field::NameKey { key, value, .. } => { let property_name = key.token().to_string(); - if !class.has_property(self.roblox_classes, &property_name) { + + if react_name == LibraryName::React + && ["ref", "key", "children"].contains(&property_name.as_str()) + { + continue; + } + + if !class.has_property(self.roblox_classes, &property_name) + || property_name == "Name" + { self.invalid_properties.push(InvalidProperty { class_name: name.clone(), property_name, + property_value: value.clone(), + create_element_expression: create_element_expression.clone().to_owned(), range: range(key), }); } @@ -225,7 +311,7 @@ impl<'a> Visitor for IncorrectRoactUsageVisitor<'a> { if let ast::Expression::Var(ast::Var::Expression(var_expression)) = key; if let ast::Prefix::Name(constant_roact_name) = var_expression.prefix(); - if constant_roact_name.token().to_string() == "Roact"; + if ["Roact", "React"].contains(&constant_roact_name.token().to_string().as_str()); let mut suffixes = var_expression.suffixes(); if let Some(ast::Suffix::Index(ast::Index::Dot { name: constant_event_name, .. })) = suffixes.next(); @@ -254,9 +340,9 @@ impl<'a> Visitor for IncorrectRoactUsageVisitor<'a> { for (name, expr) in node.names().iter().zip(node.expressions().iter()) { if_chain! { if let ast::Expression::Var(ast::Var::Expression(var_expr)) = expr; - if is_roact_create_element(var_expr.prefix(), &var_expr.suffixes().collect::>()); + if let Some(roact_or_react) = is_roact_or_react_create_element(var_expr.prefix(), &var_expr.suffixes().collect::>()); then { - self.definitions_of_create_element.insert(name.token().to_string()); + self.definitions_of_create_element.insert(name.token().to_string(), roact_or_react); } }; } @@ -267,6 +353,15 @@ impl<'a> Visitor for IncorrectRoactUsageVisitor<'a> { mod tests { use super::{super::test_util::test_lint, *}; + #[test] + fn test_mixed_roact_react_usage() { + test_lint( + IncorrectRoactUsageLint::new(()).unwrap(), + "roblox_incorrect_roact_usage", + "mixed_roact_react_usage", + ); + } + #[test] fn test_old_roblox_std() { test_lint( @@ -276,6 +371,15 @@ mod tests { ); } + #[test] + fn test_roblox_incorrect_react_usage() { + test_lint( + IncorrectRoactUsageLint::new(()).unwrap(), + "roblox_incorrect_roact_usage", + "roblox_incorrect_react_usage", + ); + } + #[test] fn test_roblox_incorrect_roact_usage() { test_lint( diff --git a/selene-lib/src/lints/roblox_suspicious_udim2_new.rs b/selene-lib/src/lints/roblox_suspicious_udim2_new.rs new file mode 100644 index 00000000..66a06479 --- /dev/null +++ b/selene-lib/src/lints/roblox_suspicious_udim2_new.rs @@ -0,0 +1,144 @@ +use super::*; +use crate::ast_util::range; +use std::convert::Infallible; + +use full_moon::{ + ast::{self, Ast}, + visitors::Visitor, +}; + +pub struct SuspiciousUDim2NewLint; + +fn create_diagnostic(mismatch: &MismatchedArgCount) -> Diagnostic { + let code = "roblox_suspicious_udim2_new"; + let message = format!( + "UDim2.new takes 4 numbers, but {} {} provided.", + mismatch.args_provided, + if mismatch.args_provided == 1 { + "was" + } else { + "were" + } + ); + let primary_label = Label::new(mismatch.call_range); + + if mismatch.args_provided <= 2 && mismatch.args_are_numbers { + Diagnostic::new_complete( + code, + message, + primary_label, + vec![if mismatch.args_are_between_0_and_1 { + "did you mean to use UDim2.fromScale instead?" + } else { + "did you mean to use UDim2.fromOffset instead?" + } + .to_owned()], + Vec::new(), + ) + } else { + Diagnostic::new(code, message, primary_label) + } +} + +impl Lint for SuspiciousUDim2NewLint { + type Config = (); + type Error = Infallible; + + const SEVERITY: Severity = Severity::Warning; + const LINT_TYPE: LintType = LintType::Correctness; + + fn new(_: Self::Config) -> Result { + Ok(SuspiciousUDim2NewLint) + } + + fn pass(&self, ast: &Ast, context: &Context, _: &AstContext) -> Vec { + if !context.is_roblox() { + return Vec::new(); + } + + let mut visitor = UDim2CountVisitor::default(); + + visitor.visit_ast(ast); + + visitor.args.iter().map(create_diagnostic).collect() + } +} + +#[derive(Default)] +struct UDim2CountVisitor { + args: Vec, +} + +struct MismatchedArgCount { + args_provided: usize, + call_range: (usize, usize), + args_are_between_0_and_1: bool, + args_are_numbers: bool, +} + +impl Visitor for UDim2CountVisitor { + fn visit_function_call(&mut self, call: &ast::FunctionCall) { + if_chain::if_chain! { + if let ast::Prefix::Name(token) = call.prefix(); + if token.token().to_string() == "UDim2"; + let mut suffixes = call.suffixes().collect::>(); + + if suffixes.len() == 2; // .new and () + let call_suffix = suffixes.pop().unwrap(); + let index_suffix = suffixes.pop().unwrap(); + + if let ast::Suffix::Index(ast::Index::Dot { name, .. }) = index_suffix; + if name.token().to_string() == "new"; + + if let ast::Suffix::Call(ast::Call::AnonymousCall( + ast::FunctionArgs::Parentheses { arguments, .. } + )) = call_suffix; + + then { + let args_provided = arguments.len(); + + if args_provided >= 4 { + return; + } + + let numbers_passed = arguments.iter().filter(|expression| { + match expression { + ast::Expression::Value { value, .. } => matches!(&**value, ast::Value::Number(_)), + _ => false, + } + }).count(); + + // Prevents false positives for UDim2.new(UDim.new(), UDim.new()) + if args_provided == 2 && numbers_passed == 0 { + return; + }; + + self.args.push(MismatchedArgCount { + call_range: range(call), + args_provided, + args_are_between_0_and_1: arguments.iter().all(|argument| { + match argument.to_string().parse::() { + Ok(number) => (0.0..=1.0).contains(&number), + Err(_) => false, + } + }), + args_are_numbers: numbers_passed == args_provided, + }); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{super::test_util::test_lint, *}; + + #[test] + fn test_roblox_suspicious_udim2_new() { + test_lint( + SuspiciousUDim2NewLint::new(()).unwrap(), + "roblox_suspicious_udim2_new", + "roblox_suspicious_udim2_new", + ); + } +} diff --git a/selene-lib/src/lints/standard_library.rs b/selene-lib/src/lints/standard_library.rs index 10cf9c78..a87424e1 100644 --- a/selene-lib/src/lints/standard_library.rs +++ b/selene-lib/src/lints/standard_library.rs @@ -559,7 +559,17 @@ impl Visitor for StandardLibraryVisitor<'_> { if (arguments_length < expected_args && !maybe_more_arguments) || (!vararg && arguments_length > max_args) { - self.diagnostics.push(Diagnostic::new( + let required_param_message = function + .arguments + .get(arguments_length) + .into_iter() + .filter_map(|arg| match &arg.required { + Required::Required(Some(message)) => Some(message.clone()), + _ => None, + }) + .collect(); + + self.diagnostics.push(Diagnostic::new_complete( "incorrect_standard_library_use", format!( "standard library function `{}` requires {} parameters, {} passed", @@ -568,6 +578,8 @@ impl Visitor for StandardLibraryVisitor<'_> { argument_types.len(), ), Label::from_node(call, None), + required_param_message, + Vec::new(), )); } diff --git a/selene-lib/tests/lints/deprecated/deprecated_functions.stderr b/selene-lib/tests/lints/deprecated/deprecated_functions.stderr index a1864d6b..724687ff 100644 --- a/selene-lib/tests/lints/deprecated/deprecated_functions.stderr +++ b/selene-lib/tests/lints/deprecated/deprecated_functions.stderr @@ -12,7 +12,7 @@ error[deprecated]: standard library function `table.getn` is deprecated 2 │ print(table.getn(x)) │ ^^^^^^^^^^^^^ │ - = `table.getn` has been superceded by #. + = `table.getn` has been superseded by #. = try: #x error[deprecated]: standard library function `table.foreach` is deprecated diff --git a/selene-lib/tests/lints/empty_loop/empty_loop.lua b/selene-lib/tests/lints/empty_loop/empty_loop.lua new file mode 100644 index 00000000..cc63169a --- /dev/null +++ b/selene-lib/tests/lints/empty_loop/empty_loop.lua @@ -0,0 +1,66 @@ +for _ = 1, 10 do + return "Should not warn" +end + +for _ = 1, 10 do + print("Should not warn") +end + +for _ = 1, 10 do + -- Should warn +end + +for _ = 1, 10 do + --[[ + Should warn + ]] +end + +for _ = 1, 10 do + + + +end + +for _ in pairs({}) do + return "Should not warn" +end + +for _ in pairs({}) do +end + +for _ in ipairs({}) do + return "Should not warn" +end + +for _ in ipairs({}) do +end + +for _ in {} do + return "Should not warn" +end + +for _ in {} do +end + +for _ in a() do + return "Should not warn" +end + +for _ in a() do +end + +while true do + return "Should not warn" +end + +while true do +end + +repeat + return "Should not warn" +until true + +repeat +until true +-- comment here shouldn't break anything diff --git a/selene-lib/tests/lints/empty_loop/empty_loop.stderr b/selene-lib/tests/lints/empty_loop/empty_loop.stderr new file mode 100644 index 00000000..de0edc12 --- /dev/null +++ b/selene-lib/tests/lints/empty_loop/empty_loop.stderr @@ -0,0 +1,70 @@ +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:9:1 + │ + 9 │ ╭ for _ = 1, 10 do +10 │ │ -- Should warn +11 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:13:1 + │ +13 │ ╭ for _ = 1, 10 do +14 │ │ --[[ +15 │ │ Should warn +16 │ │ ]] +17 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:19:1 + │ +19 │ ╭ for _ = 1, 10 do +20 │ │ +21 │ │ +22 │ │ +23 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:29:1 + │ +29 │ ╭ for _ in pairs({}) do +30 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:36:1 + │ +36 │ ╭ for _ in ipairs({}) do +37 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:43:1 + │ +43 │ ╭ for _ in {} do +44 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:50:1 + │ +50 │ ╭ for _ in a() do +51 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:57:1 + │ +57 │ ╭ while true do +58 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop.lua:64:1 + │ +64 │ ╭ repeat +65 │ │ until true + │ ╰──────────^ + diff --git a/selene-lib/tests/lints/empty_loop/empty_loop_comments.lua b/selene-lib/tests/lints/empty_loop/empty_loop_comments.lua new file mode 100644 index 00000000..4e29d25b --- /dev/null +++ b/selene-lib/tests/lints/empty_loop/empty_loop_comments.lua @@ -0,0 +1,66 @@ +for _ = 1, 10 do + return "Should not warn" +end + +for _ = 1, 10 do + print("Should not warn") +end + +for _ = 1, 10 do + -- Should not warn +end + +for _ = 1, 10 do + --[[ + Should not warn + ]] +end + +for _ = 1, 10 do + + + +end + +for _ in pairs({}) do + return "Should not warn" +end + +for _ in pairs({}) do +end + +for _ in ipairs({}) do + return "Should not warn" +end + +for _ in ipairs({}) do +end + +for _ in {} do + return "Should not warn" +end + +for _ in {} do +end + +for _ in a() do + return "Should not warn" +end + +for _ in a() do +end + +while true do + return "Should not warn" +end + +while true do +end + +repeat + return "Should not warn" +until true + +repeat +until true +-- comment here shouldn't break anything diff --git a/selene-lib/tests/lints/empty_loop/empty_loop_comments.stderr b/selene-lib/tests/lints/empty_loop/empty_loop_comments.stderr new file mode 100644 index 00000000..9128ae0f --- /dev/null +++ b/selene-lib/tests/lints/empty_loop/empty_loop_comments.stderr @@ -0,0 +1,52 @@ +error[empty_loop]: empty loop block + ┌─ empty_loop_comments.lua:19:1 + │ +19 │ ╭ for _ = 1, 10 do +20 │ │ +21 │ │ +22 │ │ +23 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop_comments.lua:29:1 + │ +29 │ ╭ for _ in pairs({}) do +30 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop_comments.lua:36:1 + │ +36 │ ╭ for _ in ipairs({}) do +37 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop_comments.lua:43:1 + │ +43 │ ╭ for _ in {} do +44 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop_comments.lua:50:1 + │ +50 │ ╭ for _ in a() do +51 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop_comments.lua:57:1 + │ +57 │ ╭ while true do +58 │ │ end + │ ╰───^ + +error[empty_loop]: empty loop block + ┌─ empty_loop_comments.lua:64:1 + │ +64 │ ╭ repeat +65 │ │ until true + │ ╰──────────^ + diff --git a/selene-lib/tests/lints/global_usage/global_usage_ignore.lua b/selene-lib/tests/lints/global_usage/global_usage_ignore.lua new file mode 100644 index 00000000..fe75274a --- /dev/null +++ b/selene-lib/tests/lints/global_usage/global_usage_ignore.lua @@ -0,0 +1,2 @@ +_G._foo_ = 1 +_G.bar = 1 diff --git a/selene-lib/tests/lints/global_usage/global_usage_ignore.stderr b/selene-lib/tests/lints/global_usage/global_usage_ignore.stderr new file mode 100644 index 00000000..fcb5d64c --- /dev/null +++ b/selene-lib/tests/lints/global_usage/global_usage_ignore.stderr @@ -0,0 +1,6 @@ +error[global_usage]: use of `_G` is not allowed, structure your code in a more idiomatic way + ┌─ global_usage_ignore.lua:2:1 + │ +2 │ _G.bar = 1 + │ ^^ + diff --git a/selene-lib/tests/lints/manual_table_clone/false_positive.lua b/selene-lib/tests/lints/manual_table_clone/false_positive.lua index 52961fd4..200e62a6 100644 --- a/selene-lib/tests/lints/manual_table_clone/false_positive.lua +++ b/selene-lib/tests/lints/manual_table_clone/false_positive.lua @@ -39,6 +39,34 @@ local function falsePositive3(t) return result end +local result4 = {} +local function falsePositive4(t) + for key, value in t do + result3[key] = value + end +end + +local result5 = {} +local function falsePositive5(t) + local function f() end + + for key, value in t do + result4[key] = value + end +end + +local function falsePositive6(t) + local result = {} + + if b then return end + + if a then + for key, value in t do + result4[key] = value + end + end +end + local function notFalsePositive1(t) local result = {} diff --git a/selene-lib/tests/lints/manual_table_clone/false_positive.stderr b/selene-lib/tests/lints/manual_table_clone/false_positive.stderr index 356c9224..1af67e80 100644 --- a/selene-lib/tests/lints/manual_table_clone/false_positive.stderr +++ b/selene-lib/tests/lints/manual_table_clone/false_positive.stderr @@ -1,24 +1,24 @@ error[manual_table_clone]: manual implementation of table.clone - ┌─ false_positive.lua:49:2 + ┌─ false_positive.lua:77:2 │ -43 │ local result = {} +71 │ local result = {} │ ----------------- remove this definition · -49 │ ╭ for key, value in pairs(t) do -50 │ │ result[key] = value -51 │ │ end +77 │ ╭ for key, value in pairs(t) do +78 │ │ result[key] = value +79 │ │ end │ ╰───────^ │ = try `local result = table.clone(t)` error[manual_table_clone]: manual implementation of table.clone - ┌─ false_positive.lua:58:3 + ┌─ false_positive.lua:86:3 │ -58 │ ╭ local result = {} -59 │ │ -60 │ │ for key, value in pairs(t) do -61 │ │ result[key] = value -62 │ │ end +86 │ ╭ local result = {} +87 │ │ +88 │ │ for key, value in pairs(t) do +89 │ │ result[key] = value +90 │ │ end │ ╰───────────^ │ = try `local result = table.clone(t)` diff --git a/selene-lib/tests/lints/mixed_table/mixed_table.lua b/selene-lib/tests/lints/mixed_table/mixed_table.lua new file mode 100644 index 00000000..54217795 --- /dev/null +++ b/selene-lib/tests/lints/mixed_table/mixed_table.lua @@ -0,0 +1,53 @@ +local bad = { + "", + a = b, +} + +bad = { + {}, + [a] = b, +} + +bad = { + a, + [""] = b, +} + +-- This is technically not a mixed table, but it's formatted like it harming readability +-- so it should still be linted +bad = { + 1, + [2] = b, +} + +bad = { + [a] = b, + [c] = d, + "", +} + +bad({ + a = b, + "", + c = d, +}) + +local good = { + a = b, + c = d, +} + +good = { + "", + a, +} + +good = { + [1] = a, + [3] = b, +} + +good({ + a = b, + c = d, +}) diff --git a/selene-lib/tests/lints/mixed_table/mixed_table.stderr b/selene-lib/tests/lints/mixed_table/mixed_table.stderr new file mode 100644 index 00000000..b557beaa --- /dev/null +++ b/selene-lib/tests/lints/mixed_table/mixed_table.stderr @@ -0,0 +1,54 @@ +error[mixed_table]: mixed tables are not allowed + ┌─ mixed_table.lua:2:5 + │ +2 │ ╭ "", +3 │ │ a = b, + │ ╰─────────^ + │ + = help: change this table to either an array or dictionary + +error[mixed_table]: mixed tables are not allowed + ┌─ mixed_table.lua:7:5 + │ +7 │ ╭ {}, +8 │ │ [a] = b, + │ ╰───────────^ + │ + = help: change this table to either an array or dictionary + +error[mixed_table]: mixed tables are not allowed + ┌─ mixed_table.lua:12:5 + │ +12 │ ╭ a, +13 │ │ [""] = b, + │ ╰────────────^ + │ + = help: change this table to either an array or dictionary + +error[mixed_table]: mixed tables are not allowed + ┌─ mixed_table.lua:19:5 + │ +19 │ ╭ 1, +20 │ │ [2] = b, + │ ╰───────────^ + │ + = help: change this table to either an array or dictionary + +error[mixed_table]: mixed tables are not allowed + ┌─ mixed_table.lua:25:5 + │ +25 │ ╭ [c] = d, +26 │ │ "", + │ ╰──────^ + │ + = help: change this table to either an array or dictionary + +error[mixed_table]: mixed tables are not allowed + ┌─ mixed_table.lua:30:5 + │ +30 │ ╭ a = b, +31 │ │ "", + │ ╰──────^ + │ + = help: change this table to either an array or dictionary + diff --git a/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.lua b/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.lua new file mode 100644 index 00000000..23c15151 --- /dev/null +++ b/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.lua @@ -0,0 +1,18 @@ +local e = React.createElement +local x = Roact.createElement + +e("Frame", { + key = "", + ref = a, + children = {}, + Name = "", + ThisPropertyDoesntExist = true, +}) + +x("Frame", { + key = "", + ref = a, + children = {}, + Name = "", + ThisPropertyDoesntExist = true, +}) diff --git a/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.std.yml b/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.std.yml new file mode 100644 index 00000000..410055f3 --- /dev/null +++ b/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.std.yml @@ -0,0 +1,13 @@ +--- +name: roblox +roblox_classes: + Frame: + superclass: GuiObject + properties: [] + events: [] + GuiObject: + superclass: Instance + properties: + - Size + events: + - InputBegan diff --git a/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.stderr b/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.stderr new file mode 100644 index 00000000..e13915ac --- /dev/null +++ b/selene-lib/tests/lints/roblox_incorrect_roact_usage/mixed_roact_react_usage.stderr @@ -0,0 +1,46 @@ +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ mixed_roact_react_usage.lua:8:5 + │ +8 │ Name = "", + │ ^^^^ + │ + = try: [""] = e(...) + +error[roblox_incorrect_roact_usage]: `ThisPropertyDoesntExist` is not a property of `Frame` + ┌─ mixed_roact_react_usage.lua:9:5 + │ +9 │ ThisPropertyDoesntExist = true, + │ ^^^^^^^^^^^^^^^^^^^^^^^ + +error[roblox_incorrect_roact_usage]: `key` is not a property of `Frame` + ┌─ mixed_roact_react_usage.lua:13:5 + │ +13 │ key = "", + │ ^^^ + +error[roblox_incorrect_roact_usage]: `ref` is not a property of `Frame` + ┌─ mixed_roact_react_usage.lua:14:5 + │ +14 │ ref = a, + │ ^^^ + +error[roblox_incorrect_roact_usage]: `children` is not a property of `Frame` + ┌─ mixed_roact_react_usage.lua:15:5 + │ +15 │ children = {}, + │ ^^^^^^^^ + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ mixed_roact_react_usage.lua:16:5 + │ +16 │ Name = "", + │ ^^^^ + │ + = try: [""] = x(...) + +error[roblox_incorrect_roact_usage]: `ThisPropertyDoesntExist` is not a property of `Frame` + ┌─ mixed_roact_react_usage.lua:17:5 + │ +17 │ ThisPropertyDoesntExist = true, + │ ^^^^^^^^^^^^^^^^^^^^^^^ + diff --git a/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.lua b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.lua new file mode 100644 index 00000000..016beb6d --- /dev/null +++ b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.lua @@ -0,0 +1,66 @@ +React.createElement("Frame", { + ThisPropertyDoesntExist = true, + Size = UDim2.new(1, 0, 1, 0), + ref = true, + children = true, + key = true, + + [React.Event.InputBegan] = function() + end, + + [React.Event.ThisEventDoesntExist] = function() + end, +}) + +local e = React.createElement + +e("Frame", { + Size = UDim2.new(1, 0, 1, 0), + ThisPropertyDoesntExist = true, +}) + +e("ThisDoesntExist", {}) + +e(Components.FooComponent, { + Foo = 1, +}) + +call("foo", {}) + +e("ThisDoesntExist") + +e(Components.FooComponent, { + Name = "Can be passed", +}) + +React.createElement(Components.FooComponent, { + Name = "Can be passed", +}) + +e("Frame", { + Name = "Should not be passed", +}) + +e("Frame", { + Name = "Should.not.be.passed", +}) + +e("Frame", { + Name = "0Should0not0be0passed", +}) + +e("Frame", { + Name = "Should0not0be0passed", +}) + +e("Frame", { + Name = "_Should_not_be_passed", +}) + +e("Frame", { + Name = "ShouldNotBePassed", +}) + +React.createElement("Frame", { + Name = a, +}) diff --git a/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.std.yml b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.std.yml new file mode 100644 index 00000000..410055f3 --- /dev/null +++ b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.std.yml @@ -0,0 +1,13 @@ +--- +name: roblox +roblox_classes: + Frame: + superclass: GuiObject + properties: [] + events: [] + GuiObject: + superclass: Instance + properties: + - Size + events: + - InputBegan diff --git a/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.stderr b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.stderr new file mode 100644 index 00000000..36d517a4 --- /dev/null +++ b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_react_usage.stderr @@ -0,0 +1,86 @@ +error[roblox_incorrect_roact_usage]: `ThisPropertyDoesntExist` is not a property of `Frame` + ┌─ roblox_incorrect_react_usage.lua:2:5 + │ +2 │ ThisPropertyDoesntExist = true, + │ ^^^^^^^^^^^^^^^^^^^^^^^ + +error[roblox_incorrect_roact_usage]: `ThisEventDoesntExist` is not a valid event for `Frame` + ┌─ roblox_incorrect_react_usage.lua:11:5 + │ +11 │ [React.Event.ThisEventDoesntExist] = function() + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[roblox_incorrect_roact_usage]: `ThisPropertyDoesntExist` is not a property of `Frame` + ┌─ roblox_incorrect_react_usage.lua:19:5 + │ +19 │ ThisPropertyDoesntExist = true, + │ ^^^^^^^^^^^^^^^^^^^^^^^ + +error[roblox_incorrect_roact_usage]: `ThisDoesntExist` is not a valid class + ┌─ roblox_incorrect_react_usage.lua:22:3 + │ +22 │ e("ThisDoesntExist", {}) + │ ^^^^^^^^^^^^^^^^^ + +error[roblox_incorrect_roact_usage]: `ThisDoesntExist` is not a valid class + ┌─ roblox_incorrect_react_usage.lua:30:3 + │ +30 │ e("ThisDoesntExist") + │ ^^^^^^^^^^^^^^^^^ + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_react_usage.lua:41:5 + │ +41 │ Name = "Should not be passed", + │ ^^^^ + │ + = try: ["Should not be passed"] = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_react_usage.lua:45:5 + │ +45 │ Name = "Should.not.be.passed", + │ ^^^^ + │ + = try: ["Should.not.be.passed"] = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_react_usage.lua:49:5 + │ +49 │ Name = "0Should0not0be0passed", + │ ^^^^ + │ + = try: ["0Should0not0be0passed"] = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_react_usage.lua:53:5 + │ +53 │ Name = "Should0not0be0passed", + │ ^^^^ + │ + = try: Should0not0be0passed = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_react_usage.lua:57:5 + │ +57 │ Name = "_Should_not_be_passed", + │ ^^^^ + │ + = try: _Should_not_be_passed = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_react_usage.lua:61:5 + │ +61 │ Name = "ShouldNotBePassed", + │ ^^^^ + │ + = try: ShouldNotBePassed = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_react_usage.lua:65:5 + │ +65 │ Name = a, + │ ^^^^ + │ + = try: [a] = React.createElement(...) + diff --git a/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_roact_usage.lua b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_roact_usage.lua index 2131acf9..3f67c9f6 100644 --- a/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_roact_usage.lua +++ b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_roact_usage.lua @@ -1,6 +1,9 @@ Roact.createElement("Frame", { ThisPropertyDoesntExist = true, Size = UDim2.new(1, 0, 1, 0), + ref = true, + children = true, + key = true, [Roact.Event.InputBegan] = function() end, @@ -25,3 +28,39 @@ e(Components.FooComponent, { call("foo", {}) e("ThisDoesntExist") + +e(Components.FooComponent, { + Name = "Can be passed", +}) + +Roact.createElement(Components.FooComponent, { + Name = "Can be passed", +}) + +e("Frame", { + Name = "Should not be passed", +}) + +e("Frame", { + Name = "Should.not.be.passed", +}) + +e("Frame", { + Name = "0Should0not0be0passed", +}) + +e("Frame", { + Name = "Should0not0be0passed", +}) + +e("Frame", { + Name = "_Should_not_be_passed", +}) + +e("Frame", { + Name = "ShouldNotBePassed", +}) + +Roact.createElement("Frame", { + Name = a, +}) diff --git a/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_roact_usage.stderr b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_roact_usage.stderr index 79db86bf..6ddc2292 100644 --- a/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_roact_usage.stderr +++ b/selene-lib/tests/lints/roblox_incorrect_roact_usage/roblox_incorrect_roact_usage.stderr @@ -4,27 +4,101 @@ error[roblox_incorrect_roact_usage]: `ThisPropertyDoesntExist` is not a property 2 │ ThisPropertyDoesntExist = true, │ ^^^^^^^^^^^^^^^^^^^^^^^ -error[roblox_incorrect_roact_usage]: `ThisEventDoesntExist` is not a valid event for `Frame` - ┌─ roblox_incorrect_roact_usage.lua:8:5 +error[roblox_incorrect_roact_usage]: `ref` is not a property of `Frame` + ┌─ roblox_incorrect_roact_usage.lua:4:5 + │ +4 │ ref = true, + │ ^^^ + +error[roblox_incorrect_roact_usage]: `children` is not a property of `Frame` + ┌─ roblox_incorrect_roact_usage.lua:5:5 + │ +5 │ children = true, + │ ^^^^^^^^ + +error[roblox_incorrect_roact_usage]: `key` is not a property of `Frame` + ┌─ roblox_incorrect_roact_usage.lua:6:5 │ -8 │ [Roact.Event.ThisEventDoesntExist] = function() - │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +6 │ key = true, + │ ^^^ + +error[roblox_incorrect_roact_usage]: `ThisEventDoesntExist` is not a valid event for `Frame` + ┌─ roblox_incorrect_roact_usage.lua:11:5 + │ +11 │ [Roact.Event.ThisEventDoesntExist] = function() + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error[roblox_incorrect_roact_usage]: `ThisPropertyDoesntExist` is not a property of `Frame` - ┌─ roblox_incorrect_roact_usage.lua:16:5 + ┌─ roblox_incorrect_roact_usage.lua:19:5 │ -16 │ ThisPropertyDoesntExist = true, +19 │ ThisPropertyDoesntExist = true, │ ^^^^^^^^^^^^^^^^^^^^^^^ error[roblox_incorrect_roact_usage]: `ThisDoesntExist` is not a valid class - ┌─ roblox_incorrect_roact_usage.lua:19:3 + ┌─ roblox_incorrect_roact_usage.lua:22:3 │ -19 │ e("ThisDoesntExist", {}) +22 │ e("ThisDoesntExist", {}) │ ^^^^^^^^^^^^^^^^^ error[roblox_incorrect_roact_usage]: `ThisDoesntExist` is not a valid class - ┌─ roblox_incorrect_roact_usage.lua:27:3 + ┌─ roblox_incorrect_roact_usage.lua:30:3 │ -27 │ e("ThisDoesntExist") +30 │ e("ThisDoesntExist") │ ^^^^^^^^^^^^^^^^^ +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_roact_usage.lua:41:5 + │ +41 │ Name = "Should not be passed", + │ ^^^^ + │ + = try: ["Should not be passed"] = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_roact_usage.lua:45:5 + │ +45 │ Name = "Should.not.be.passed", + │ ^^^^ + │ + = try: ["Should.not.be.passed"] = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_roact_usage.lua:49:5 + │ +49 │ Name = "0Should0not0be0passed", + │ ^^^^ + │ + = try: ["0Should0not0be0passed"] = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_roact_usage.lua:53:5 + │ +53 │ Name = "Should0not0be0passed", + │ ^^^^ + │ + = try: Should0not0be0passed = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_roact_usage.lua:57:5 + │ +57 │ Name = "_Should_not_be_passed", + │ ^^^^ + │ + = try: _Should_not_be_passed = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_roact_usage.lua:61:5 + │ +61 │ Name = "ShouldNotBePassed", + │ ^^^^ + │ + = try: ShouldNotBePassed = e(...) + +error[roblox_incorrect_roact_usage]: `Name` is assigned through the element's key for Roblox instances + ┌─ roblox_incorrect_roact_usage.lua:65:5 + │ +65 │ Name = a, + │ ^^^^ + │ + = try: [a] = Roact.createElement(...) + diff --git a/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.lua b/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.lua new file mode 100644 index 00000000..4a8df682 --- /dev/null +++ b/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.lua @@ -0,0 +1,15 @@ +UDim2.new(0) +UDim2.new(0.5) +UDim2.new(1) +UDim2.new(2) +UDim2.new(a) +UDim2.new(1, 1) +UDim2.new(1, 2) +UDim2.new(a, b) +UDim2.new(1, a) +UDim2.new(1, 1, 1) +UDim2.new(a, b, c) +UDim2.new(1, 1, 1, 1) +UDim2.new(a, b, c, d) +UDim2.fromOffset(1, 1) +UDim2.fromScale(1, 1) diff --git a/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.std.toml b/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.std.toml new file mode 100644 index 00000000..98886350 --- /dev/null +++ b/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.std.toml @@ -0,0 +1,2 @@ +[selene] +name = "roblox" diff --git a/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.stderr b/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.stderr new file mode 100644 index 00000000..eee324cc --- /dev/null +++ b/selene-lib/tests/lints/roblox_suspicious_udim2_new/roblox_suspicious_udim2_new.stderr @@ -0,0 +1,72 @@ +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 1 was provided. + ┌─ roblox_suspicious_udim2_new.lua:1:1 + │ +1 │ UDim2.new(0) + │ ^^^^^^^^^^^^ + │ + = did you mean to use UDim2.fromScale instead? + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 1 was provided. + ┌─ roblox_suspicious_udim2_new.lua:2:1 + │ +2 │ UDim2.new(0.5) + │ ^^^^^^^^^^^^^^ + │ + = did you mean to use UDim2.fromScale instead? + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 1 was provided. + ┌─ roblox_suspicious_udim2_new.lua:3:1 + │ +3 │ UDim2.new(1) + │ ^^^^^^^^^^^^ + │ + = did you mean to use UDim2.fromScale instead? + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 1 was provided. + ┌─ roblox_suspicious_udim2_new.lua:4:1 + │ +4 │ UDim2.new(2) + │ ^^^^^^^^^^^^ + │ + = did you mean to use UDim2.fromOffset instead? + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 1 was provided. + ┌─ roblox_suspicious_udim2_new.lua:5:1 + │ +5 │ UDim2.new(a) + │ ^^^^^^^^^^^^ + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 2 were provided. + ┌─ roblox_suspicious_udim2_new.lua:6:1 + │ +6 │ UDim2.new(1, 1) + │ ^^^^^^^^^^^^^^^ + │ + = did you mean to use UDim2.fromScale instead? + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 2 were provided. + ┌─ roblox_suspicious_udim2_new.lua:7:1 + │ +7 │ UDim2.new(1, 2) + │ ^^^^^^^^^^^^^^^ + │ + = did you mean to use UDim2.fromOffset instead? + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 2 were provided. + ┌─ roblox_suspicious_udim2_new.lua:9:1 + │ +9 │ UDim2.new(1, a) + │ ^^^^^^^^^^^^^^^ + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 3 were provided. + ┌─ roblox_suspicious_udim2_new.lua:10:1 + │ +10 │ UDim2.new(1, 1, 1) + │ ^^^^^^^^^^^^^^^^^^ + +error[roblox_suspicious_udim2_new]: UDim2.new takes 4 numbers, but 3 were provided. + ┌─ roblox_suspicious_udim2_new.lua:11:1 + │ +11 │ UDim2.new(a, b, c) + │ ^^^^^^^^^^^^^^^^^^ + diff --git a/selene-lib/tests/lints/standard_library/assert.lua b/selene-lib/tests/lints/standard_library/assert.lua index 06473166..e6442caf 100644 --- a/selene-lib/tests/lints/standard_library/assert.lua +++ b/selene-lib/tests/lints/standard_library/assert.lua @@ -6,3 +6,4 @@ assert(...) assert(true) assert(true, "message", call()) assert(true, "message", ...) +assert() diff --git a/selene-lib/tests/lints/standard_library/assert.stderr b/selene-lib/tests/lints/standard_library/assert.stderr index 25fc666c..1974ef1f 100644 --- a/selene-lib/tests/lints/standard_library/assert.stderr +++ b/selene-lib/tests/lints/standard_library/assert.stderr @@ -3,6 +3,8 @@ error[incorrect_standard_library_use]: standard library function `assert` requir │ 6 │ assert(true) │ ^^^^^^^^^^^^ + │ + = A failed assertion without a message is unhelpful to users. error[incorrect_standard_library_use]: standard library function `assert` requires 2 parameters, 3 passed ┌─ assert.lua:7:1 @@ -16,3 +18,9 @@ error[incorrect_standard_library_use]: standard library function `assert` requir 8 │ assert(true, "message", ...) │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +error[incorrect_standard_library_use]: standard library function `assert` requires 2 parameters, 0 passed + ┌─ assert.lua:9:1 + │ +9 │ assert() + │ ^^^^^^^^ + diff --git a/selene-lib/tests/lints/undefined_variable/function_overriding.lua b/selene-lib/tests/lints/undefined_variable/function_overriding.lua index f0729619..bb11e2b3 100644 --- a/selene-lib/tests/lints/undefined_variable/function_overriding.lua +++ b/selene-lib/tests/lints/undefined_variable/function_overriding.lua @@ -1 +1,7 @@ function global.name() end + +function a.b() end +function a.c() end + +function d:e() end +function d:f() end diff --git a/selene-lib/tests/lints/undefined_variable/function_overriding.stderr b/selene-lib/tests/lints/undefined_variable/function_overriding.stderr index ee9f9954..b91148c0 100644 --- a/selene-lib/tests/lints/undefined_variable/function_overriding.stderr +++ b/selene-lib/tests/lints/undefined_variable/function_overriding.stderr @@ -4,3 +4,27 @@ error[undefined_variable]: `global` is not defined 1 │ function global.name() end │ ^^^^^^ +error[undefined_variable]: `a` is not defined + ┌─ function_overriding.lua:3:10 + │ +3 │ function a.b() end + │ ^ + +error[undefined_variable]: `a` is not defined + ┌─ function_overriding.lua:4:10 + │ +4 │ function a.c() end + │ ^ + +error[undefined_variable]: `d` is not defined + ┌─ function_overriding.lua:6:10 + │ +6 │ function d:e() end + │ ^ + +error[undefined_variable]: `d` is not defined + ┌─ function_overriding.lua:7:10 + │ +7 │ function d:f() end + │ ^ + diff --git a/selene/src/main.rs b/selene/src/main.rs index 4042f991..610aeb8a 100644 --- a/selene/src/main.rs +++ b/selene/src/main.rs @@ -618,6 +618,10 @@ fn start(mut options: opts::Options) { let checker = Arc::clone(&checker); let filename = filename.to_owned(); + if !options.no_exclude && exclude_set.is_match(&filename) { + continue; + } + pool.execute(move || read_file(&checker, Path::new(&filename))); } else if metadata.is_dir() { for pattern in &options.pattern { @@ -636,7 +640,7 @@ fn start(mut options: opts::Options) { for entry in glob { match entry { Ok(path) => { - if exclude_set.is_match(&path) { + if !options.no_exclude && exclude_set.is_match(&path) { continue; } diff --git a/selene/src/opts.rs b/selene/src/opts.rs index f22259c6..1ce1bbb7 100644 --- a/selene/src/opts.rs +++ b/selene/src/opts.rs @@ -66,6 +66,9 @@ pub struct Options { #[structopt(subcommand)] pub command: Option, + + #[structopt(long)] + pub no_exclude: bool, } impl Options { diff --git a/selene/src/validate_config.rs b/selene/src/validate_config.rs index b552e306..074522ab 100644 --- a/selene/src/validate_config.rs +++ b/selene/src/validate_config.rs @@ -120,7 +120,9 @@ pub fn validate_config( ErrorRange { start, end } }); - let Err(error) = crate::standard_library::collect_standard_library(&config, config.std(), directory, &None) else { + let Err(error) = + crate::standard_library::collect_standard_library(&config, config.std(), directory, &None) + else { return Ok(()); }; @@ -185,16 +187,16 @@ mod tests { let Err(validate_result) = validate_config(&config_path, &config_contents, &validate_config_test.path()) - else { - tests_pass = false; + else { + tests_pass = false; - eprintln!( - "{} did not error", - validate_config_test.file_name().to_string_lossy() - ); + eprintln!( + "{} did not error", + validate_config_test.file_name().to_string_lossy() + ); - continue; - }; + continue; + }; let mut rich_output_buffer = termcolor::NoColor::new(Vec::new()); validate_result