From 6c1b626a0f2f8eacfc73d1ca4ac76ce466dd9fc2 Mon Sep 17 00:00:00 2001 From: Mantas Kantautas Date: Sat, 31 Oct 2026 03:52:53 +0200 Subject: [PATCH] Fix sort requires moving requires above `local require = require(...)` A require that rebinds `require` itself is no longer sortable, so it stays in place and splits its block. The requires after it, which call the new `require`, stay below it. --- CHANGELOG.md | 4 +++ README.md | 3 ++ src/sort_requires.rs | 19 ++++++++++ .../inputs-sort-requires/rebound-require.lua | 7 ++++ ...ts__sort_requires@rebound-require.lua.snap | 13 +++++++ tests/tests.rs | 35 +++++++++++++++++++ 6 files changed, 81 insertions(+) create mode 100644 tests/inputs-sort-requires/rebound-require.lua create mode 100644 tests/snapshots/tests__sort_requires@rebound-require.lua.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ac6dc18d..6bbabdbda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fixed sort requires moving requires above a `local require = require(...)` statement they depend on: a statement that rebinds `require` now stays in place and splits its block ([#993](https://github.com/JohnnyMorganz/StyLua/issues/993)) + ## [2.6.0] - 2026-10-10 ### Added diff --git a/README.md b/README.md index 8f1111bdd..e7a53e210 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,9 @@ StyLua only considers requires of the form `local NAME = require(EXPR)`, or `con and sorts lexicographically based on `NAME`. (StyLua can also sort Roblox services of the form `local NAME = game:GetService(EXPR)`, or `const NAME = game:GetService(EXPR)` in Luau) +A require which rebinds `require` itself, such as `local require = require(EXPR)`, is never moved. +It splits the block, so the requires after it, which call the new `require`, stay below it. + Requires sorting is off by default. To enable it, add the following to your `stylua.toml`: ```toml diff --git a/src/sort_requires.rs b/src/sort_requires.rs index a8a78ebd2..c6b68d09f 100644 --- a/src/sort_requires.rs +++ b/src/sort_requires.rs @@ -14,6 +14,9 @@ //! This allows us to solve the assumption of depending on local variables //! (if there is a local variable in between requires, it would split them into two separate blocks, //! so a require will always be after any local variable it uses) +//! - A require which rebinds `require` itself (e.g. `local require = require(...)`) is not sorted: +//! the requires after it call the new local, so it stays in place and splits the block, +//! keeping those requires below it. //! - Blocks remain in-place in the file. use full_moon::{ @@ -81,6 +84,11 @@ fn get_sortable_assignment_parts( let expression_kind = get_expression_kind(expression)?; let variable_name = extract_identifier_from_token(name).expect("require is stored as non-identifier"); + // `local require = require(...)` rebinds the function that the following requires call. + // Moving it within its block would change which `require` they use, so it is not sortable. + if expression_kind == GroupKind::Require && variable_name == "require" { + return None; + } let current_line = name.start_position().unwrap().line(); Some((variable_name, expression_kind, current_line)) @@ -333,4 +341,15 @@ mod tests { assert_eq!(get_expression_kind(expression), Some(GroupKind::Require)); } + + #[test] + fn rebinding_require_is_not_sortable() { + let ast = full_moon::parse("local require = require(EXPR)").unwrap(); + let stmt = ast.nodes().stmts_with_semicolon().next().unwrap(); + assert_eq!(get_sortable_assignment(stmt), None); + + let ast = full_moon::parse("local other = require(EXPR)").unwrap(); + let stmt = ast.nodes().stmts_with_semicolon().next().unwrap(); + assert!(get_sortable_assignment(stmt).is_some()); + } } diff --git a/tests/inputs-sort-requires/rebound-require.lua b/tests/inputs-sort-requires/rebound-require.lua new file mode 100644 index 000000000..2ebf6bcb9 --- /dev/null +++ b/tests/inputs-sort-requires/rebound-require.lua @@ -0,0 +1,7 @@ +local require = require(ReplicatedStorage:WaitForChild("Sonar")) +local DisplayUtils = require("DisplayUtils") +local Maid = require("Maid") +local FormatNumber = require("FormatNumber") +local GameUtils = require("GameUtils") +local RemoteUtils = require("RemoteUtils") +local Button = require("Button") diff --git a/tests/snapshots/tests__sort_requires@rebound-require.lua.snap b/tests/snapshots/tests__sort_requires@rebound-require.lua.snap new file mode 100644 index 000000000..7b733cb8d --- /dev/null +++ b/tests/snapshots/tests__sort_requires@rebound-require.lua.snap @@ -0,0 +1,13 @@ +--- +source: tests/tests.rs +expression: "format_code(&contents, Config\n{ sort_requires: SortRequiresConfig { enabled: true }, ..Config::default() },\nNone, OutputVerification::None).unwrap()" +input_file: tests/inputs-sort-requires/rebound-require.lua +--- +local require = require(ReplicatedStorage:WaitForChild("Sonar")) +local Button = require("Button") +local DisplayUtils = require("DisplayUtils") +local FormatNumber = require("FormatNumber") +local GameUtils = require("GameUtils") +local Maid = require("Maid") +local RemoteUtils = require("RemoteUtils") + diff --git a/tests/tests.rs b/tests/tests.rs index 908a390f9..ba12a57a9 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -366,6 +366,41 @@ const SplitA = require("SplitA") ); } +#[test] +#[cfg(feature = "luau")] +fn test_sort_requires_luau_rebound_require() { + let input = r#"local Beta = require("Beta") +local utils = require("utils") +local Alpha = require("Alpha") +const require = require(script.Loader) :: any +local Zebra = require("Zebra") +const Apple = require("Apple") +"#; + + let output = format_code( + input, + Config { + syntax: LuaVersion::Luau, + sort_requires: SortRequiresConfig { enabled: true }, + ..Config::default() + }, + None, + OutputVerification::None, + ) + .unwrap(); + + assert_eq!( + output, + r#"local Alpha = require("Alpha") +local Beta = require("Beta") +local utils = require("utils") +const require = require(script.Loader) :: any +const Apple = require("Apple") +local Zebra = require("Zebra") +"# + ); +} + #[test] fn test_crlf_in_multiline_comments() { // We need to do this outside of insta since it normalises line endings to LF