Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions src/sort_requires.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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());
}
}
7 changes: 7 additions & 0 deletions tests/inputs-sort-requires/rebound-require.lua
Original file line number Diff line number Diff line change
@@ -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")
13 changes: 13 additions & 0 deletions tests/snapshots/tests__sort_requires@rebound-require.lua.snap
Original file line number Diff line number Diff line change
@@ -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")

35 changes: 35 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down