Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..f1731109 --- /dev/null +++ b/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/404.html b/404.html new file mode 100644 index 00000000..7c1b6e18 --- /dev/null +++ b/404.html @@ -0,0 +1,222 @@ + + +
+ + +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +Redirecting to... /usage/configuration.html.
+ + diff --git a/cli/index.html b/cli/index.html new file mode 100644 index 00000000..58bb831d --- /dev/null +++ b/cli/index.html @@ -0,0 +1,237 @@ + + + + + +selene is mostly intended for use as a command line tool.
+In this section, you will learn how to use selene in this manner.
+ +selene is written in Rust, and the recommended installation method is through the Cargo package manager.
+To use Cargo, you must first install Rust. rustup.rs is a tool that makes this very easy.
+Once you have Rust installed, use either command:
+If you want the most stable version of selene
+cargo install selene
+
+(Note: Currently, the above method will give you an outdated selene, as the selene crate on Cargo is not yet maintained.)
+If you want the most up to date version of selene
+cargo install --git https://github.com/Kampfkarren/selene selene
+
+selene is built with Roblox specific lints by default. If you don't want these, type --no-default-features
after whichever command you choose.
Redirecting to... /usage/std.html.
+ + diff --git a/cli/usage.html b/cli/usage.html new file mode 100644 index 00000000..6cda9181 --- /dev/null +++ b/cli/usage.html @@ -0,0 +1,288 @@ + + + + + +If you want to get a quick understanding of the interface, simply type selene --help
.
USAGE:
+ selene [FLAGS] [OPTIONS] <files>...
+
+FLAGS:
+ -h, --help Prints help information
+ -q Display only the necessary information
+ -V, --version Prints version information
+
+OPTIONS:
+ --config <config> A toml file to configure the behavior of selene [default: selene.toml]
+ --num-threads <num-threads> Number of threads to run on, default to the numbers of logical cores on your
+ system [default: 8]
+ --pattern <pattern> A glob to match files with to check [default: **/*.lua]
+
+ARGS:
+ <files>...
+
+All unnamed inputs you give to selene will be treated as files to check for.
+If you want to check a folder of files: selene files
If you just want to check one file: selene code.lua
If you want to check multiple files/folders: selene file1 file2 file3 ...
-q
+--quiet
+Instead of the rich format, only necessary information will be displayed.
+~# selene code.lua
+warning[divide_by_zero]: dividing by zero is not allowed, use math.huge instead
+
+ ┌── code.lua:1:6 ───
+ │
+ 1 │ call(1 / 0)
+ │ ^^^^^
+ │
+
+Results:
+0 errors
+1 warnings
+0 parse errors
+
+~# selene code.lua -q
+code.lua:1:6: warning[divide_by_zero]: dividing by zero is not allowed, use math.huge instead
+
+Results:
+0 errors
+1 warnings
+0 parse errors
+
+--num-threads num-threads
+Specifies the number of threads for selene to use. Defaults to however many cores your CPU has. If you type selene --help
, you can see this number because it will show as the default for you.
--pattern pattern
+A glob 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".
selene is written in Rust, so knowledge of the ecosystem is expected.
+selene uses Full Moon to parse the Lua code losslessly, meaning whitespace and comments are preserved. You can read the full documentation for Full Moon on its docs.rs page.
+TODO: Upload selene-lib on crates.io and link the docs.rs page for that as well as throughout the rest of this article.
+In selene, lints are created in isolated modules. To start, create a file in selene-lib/src/rules
with the name of your lint. In this example, we're going to call the lint cool_lint.rs
.
Let's now understand what a lint consists of. selene takes lints in the form of structs that implement the Rule
trait. The Rule
trait expects:
Config
associated type that defines what the configuration format is expected to be. Whatever you pass must be deserializable.Error
associated type that implements std::error::Error
. This is used if configurations can be invalid (such as a parameter only being a number within a range). Most of the time, configurations cannot be invalid (other than deserializing errors, which are handled by selene), and so you can set this to std::convert::Infallible
.new
function with the signature fn new(config: Self::Config) -> Result<Self, Self::Error>
. With the selene CLI, this is called once.pass
function with the signature fn pass(&self, ast: &full_moon::ast::Ast<'static>, context: &Context) -> Vec<Diagnostic>
. The ast
argument is the Full Moon representation of the code, and the context provides optional additional information, such as the standard library being used. Any Diagnostic
structs returned here are displayed to the user.severity
function with the signature fn severity(&self) -> Severity
. Returns either Severity::Error
or Severity::Warning
. Use Error
if the code is positively impossible to be correct. The &self
is only provided due to limitations of Rust--the function should be completely constant and pure.rule_type
function with the signature fn rule_type(&self) -> RuleType
. Returns either Complexity
, Correctness
, Performance
, or Style
. So far not used for anything. Has the same gotcha as severity
in relation to &self
.For our purposes, we're going to write:
+use super::*;
+use std::convert::Infallible;
+
+struct CoolLint;
+
+impl Rule for CoolLint {
+ type Config = ();
+ type Error = Infallible;
+
+ fn new(_: Self::Config) -> Result<Self, Self::Error> {
+ Ok(CoolLint)
+ }
+
+ fn pass(&self, ast: &Ast, _: &Context) -> Vec<Diagnostic> {
+ unimplemented!()
+ }
+
+ fn severity(&self) -> Severity {
+ Severity::Warning
+ }
+
+ fn rule_type(&self) -> RuleType {
+ RuleType::Style
+ }
+}
+
+The implementation of pass
is completely up to you, but there are a few common patterns.
divide_by_zero
and suspicious_reverse_loop
for straight forward examples.ScopeManager
struct to lint based off of usage of variables and references. See shadowing
and global_usage
.Now that we have our lint, we have to make sure selene actually knows to use it. There are two places you need to update.
+In selene-lib/src/lib.rs, search for use_rules!
. You will see something such as:
use_rules! {
+ almost_swapped: rules::almost_swapped::AlmostSwappedLint,
+ divide_by_zero: rules::divide_by_zero::DivideByZeroLint,
+ empty_if: rules::empty_if::EmptyIfLint,
+ ...
+}
+
+Put your lint in this list (alphabetical order) as the following format:
+lint_name: rules::module_name::LintObject,
+
+For us, this would be:
+cool_lint: rules::cool_lint::CoolLint,
+
+Next, in selene-lib/src/rules.rs
, search for pub mod
, and you will see:
pub mod almost_swapped;
+pub mod divide_by_zero;
+pub mod empty_if;
+...
+
+Put your module name in this list, also in alphabetical order.
+pub mod almost_swapped;
+pub mod cool_lint;
+pub mod divide_by_zero;
+pub mod empty_if;
+...
+
+And we're done! You should be able to cargo build --bin selene
and be able to use your new lint.
The selene codebase uses tests extensively for lints. It means we never have to actually build the CLI tool in order to test, and we can make sure we don't have any regressions. Testing is required if you want to submit your lint to the selene codebase.
+To write a simple test, create a folder in selene-lib/tests
with the name of your lint. Then, create as many .lua
files as you want to test. These should contain both cases that do and do not lint. For our cases, we're going to assume our test is called cool_lint.lua
.
Then, in your lint module, add at the bottom:
+#[cfg(test)]
+mod tests {
+ use super::{super::test_util::test_lint, *};
+
+ #[test]
+ fn test_cool_lint() {
+ test_lint(
+ CoolLint::new(()).unwrap(),
+ "cool_lint",
+ "cool_lint",
+ );
+ }
+}
+
+Let's discuss what this code means, assuming you're familiar with the way tests are written and performed in Rust.
+The test_lint
function is the easiest way to test that a lint works. It'll search the source code we made before, run selene on it (only your lint), and check its results with the existing [filename].stderr
file, or create one if it does not yet exist.
The first argument is the lint object to use. CoolLint::new(())
just means "create CoolLint
with a configuration of ()
". If your lint specifies a configuration, this will instead be something such as CoolLintConfig::default()
or whatever you're specifically testing.
The .unwrap()
is just because CoolLint::new
returns a Result
. If you want to test configuration errors, you can avoid test_lint
altogether and just test CoolLint::new(...).is_err()
directly.
The first "cool_lint"
is the name of the folder we created. The second "cool_lint"
is the name of the Lua file we created.
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 rule, 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 to use. If you do not, the Lua 5.1 standard library will be used.
This step is only if you are contributing to the selene codebase, and not just writing personal lints (though I'm sure your other programmers would love if you did this).
+To document a new lint, edit docs/src/SUMMARY.md
, and add your lint to the table of contents along the rest. As with everything else, make sure it's in alphabetical order.
Then, edit the markdown file it creates (if you have mdbook serve
on, it'll create it for you), and write it in this format:
# rule_name
+## What it does
+Explain what your lint does, simply.
+
+## Why this is bad
+Explain why a user would want to lint this.
+
+## Configuration
+Specify any configuration if it exists.
+
+## Example
+```lua
+-- Bad code here
+```
+
+...should be written as...
+
+```lua
+-- Good code here
+```
+
+## Remarks
+If there's anything else a user should know when using this lint, write it here.
+
+This isn't a strict format, and you can mess with it as appropriate. For example, standard_library
does not have a "Why this is bad" section as not only is it a very encompassing rule, but it should be fairly obvious. Many rules don't specify a "...should be written as..." as it is either something with various potential fixes (such as global_usage
) or because the "good code" is just removing parts entirely (such as unbalanced_assignments
).
selene is a command line tool designed to help write correct and idiomatic Lua code.
+selene and all its source code are licensed under the Mozilla Public License 2.0.
+ +Checks for foo = bar; bar = foo
sequences.
This looks like a failed attempt to swap.
+a = b
+b = a
+
+...should be written as...
+a, b = b, a
+
+
+ Checks for division by zero. Allows 0 / 0
as a way to get nan.
n / 0
equals math.huge
when n is positive, and -math.huge
when n is negative. Use these values directly instead, as using the / 0
way is confusing to read and non-idiomatic.
print(1 / 0)
+print(-1 / 0)
+
+...should be written as...
+print(math.huge)
+print(-math.huge)
+
+
+ Checks for empty if blocks.
+You most likely forgot to write code in there or commented it out without commenting out the if statement itself.
+comments_count
(default: false
) - A bool that determines whether or not if statements with exclusively comments are empty.
-- Each of these branches count as an empty if.
+if a then
+elseif b then
+else
+end
+
+if a then
+ -- If comments_count is true, this will not count as empty.
+end
+
+
+ Prohibits use of _G
.
_G
is global mutable state, which is heavily regarded as harmful. You should instead refactor your code to be more modular in nature.
If you are using the Roblox standard library, use of shared
is prohibited under this rule.
_G.foo = 1
+
+
+ Checks for branches in if blocks that are equivalent.
+This is most likely a copy and paste error.
+if foo then
+ print(1)
+else
+ print(1)
+end
+
+
+ Checks for branches in if blocks with equivalent conditions.
+This is most likely a copy and paste error.
+if foo then
+ print(1)
+elseif foo then
+ print(1)
+end
+
+This ignores conditions that could have side effects, such as function calls. This will not lint:
+if foo() then
+ print(1)
+elseif foo() then
+ print(1)
+end
+
+...as the result of foo()
could be different the second time it is called.
Checks for correct use of the standard library.
+for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
+
+It is highly recommended that you do not turn this lint off. If you are having standard library issues, modify your standard library instead to be correct. If it is a problem with an official standard library (Ex: the Lua 5.1 or Roblox ones), you can file an issue on GitHub.
+ +The following is the list of lints that selene will check for in your code.
+ +Checks for multiple statements on the same line.
+This can make your code difficult to read.
+one_line_if
(default: "break-return-only"
) - Defines whether or not one line if statements should be allowed. One of three options:
if x then return end
or if x then break end
is ok, but if x then call() end
is not.foo() bar() baz()
+
+...should be written as...
+foo()
+bar()
+baz()
+
+
+ Checks for conditions in the form of (expression)
.
Lua does not require these, and they are not idiomatic.
+if (x) then
+repeat until (x)
+while (x) do
+
+...should be written as...
+if x then
+repeat until x
+while x do
+
+
+ Checks for uses of Color3.new
where the arguments are not between 0 and 1.
Most likely, you are trying to use values of 0 to 255. This will not give you an error, and will silently give you the wrong color. You probably meant to use Color3.fromRGB
instead.
Color3.new(255, 0, 0)
+
+This lint is only active if you are using the Roblox standard library.
+ +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.
+This is guaranteed to fail once it is rendered. Furthermore, the createElement itself will not error--only once its mounted will it error.
+Roact.createElement("Frame", {
+ ThisPropertyDoesntExist = true,
+})
+
+Roact.createElement("BadClass", {})
+
+This lint is naive and makes several assumptions about the way you write your code. The assumptions are based on idiomatic Roact.
+Roact.createElement
directly or creating a local variable that's assigned to Roact.createElement
.undefined_variable
will still lint, however.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.
Additionally, this lint is based off of rbx_reflection. In practice, this means that if Roblox adds new properties or classes, selene will not know they exist until you update it.
+This lint is only active if you are using the Roblox standard library.
+ +Checks for overriding of variables under the same name.
+This can cause confusion when reading the code when trying to understand which variable is being used, and if you want to use the original variable you either have to redefine it under a temporary name or refactor the code that shadowed it.
+ignore_pattern
(default: "^_"
) - A regular expression that is used to specify names that are allowed to be shadowed. The default allows for variables like _
to be shadowed, as they shouldn't be used anyway.
local x = 1
+
+if foo then
+ local x = 1
+end
+
+
+ Checks for for _ = #x, 1 do
sequences without specifying a negative step.
This loop will only run at most once, instead of going in reverse. If you truly did mean to run your loop only once, just use if #x > 0
instead.
for _ = #x, 1 do
+
+...should be written as...
+for _ = #x, 1, -1 do
+
+
+ Checks for type(foo == "type")
, instead of type(foo) == "type"
.
This will always return "boolean"
, and is undoubtedly not what you intended to write.
return type(foo == "number")
+
+...should be written as...
+return type(foo) == "number"
+
+When using the Roblox standard library, this checks typeof
as well.
Checks for unbalanced assignments, such as a, b, c = 1
.
You shouldn't declare variables you're not immediately initializing on the same line as ones you are. This is most likely just forgetting to specify the rest of the variables.
+a, b, c = 1
+a = 1, 2
+
+There are a few things this lint won't catch.
+a, b, c = call()
will not lint, as call()
could return multiple values.
a, b, c = call(), 2
will, however, as you will only be using the first value of call()
. You will even receive a helpful message about this.
error[unbalanced_assignments]: values on right side don't match up to the left side of the assignment
+
+ ┌── unbalanced_assignments.lua:6:11 ───
+ │
+ 6 │ a, b, c = call(), 2
+ │ ^^^^^^^^^
+ │
+
+ ┌── unbalanced_assignments.lua:6:11 ───
+ │
+ 6 │ a, b, c = call(), 2
+ │ ------ help: if this function returns more than one value, the only first return value is actually used
+ │
+
+If nil is specified as the last value, the rest will be ignored. This means...
+a, b, c = nil
+
+...will not lint.
+ +Checks for uses of variables that are not defined.
+This is most likely a typo.
+-- vv oops!
+prinnt("hello, world!")
+
+If you are using a different standard library where a global variable is defined that selene isn't picking up on, create a standard library that specifies it.
+ +Checks for variables that are unscoped (don't have a local variable attached).
+Unscoped variables make code harder to read and debug, as well as making it harder for selene to analyze.
+ignore_pattern
(default: "^_"
) - A regular expression for variables that are allowed to be unscoped. The default allows for variables like _
to be unscoped, as they shouldn't be used anyway.
baz = 3
+
+
+ Checks for variables that are unused.
+The existence of unused variables could indicate buggy code.
+allow_unused_self
(default: true
) - A bool that determines whether not using self
in a method function (function Player:SwapWeapons()
) is allowed.
ignore_pattern
(default: "^_"
) - A regular expression for variables that are allowed to be unused. The default allows for variables like _
to be unused, as they shouldn't be used anyway.
local foo = 1
+
+If you intend to create a variable without using it, replace it with _
or something that starts with _
. You'll see this most in generic for loops.
for _, value in ipairs(list) do
+
+
+ selene is not the first Lua linter. The main inspiration behind selene is luacheck. However, the two have very little in common besides inception.
+selene:
+error[suspicious_reverse_loop]: this loop will only ever run once at most
+
+ ┌── fail.lua:1:9 ───
+ │
+ 1 │ for _ = #x, 1 do
+ │ ^^^^^
+ │
+ = help: try adding `, -1` after `1`
+
+luacheck:
+Checking fail.lua 2 warnings
+
+ fail.lua:1:1: numeric for loop goes from #(expr) down to 1 but loop step is not negative
+
+.luacheckrc
, which runs Lua.for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
+
+math.pi()
+
+...while luacheck does not.
+211
", while in selene you ignore "unbalanced_assignments
".-q
option (quiet).This is not to say selene is objectively better than luacheck, at least not yet.
+luacheck does not require much configuration to begin with, so migration should be easy.
+generate-roblox-std
? Should generate-roblox-std
be uploaded to crates.io?When writing any code, it's very easy to make silly mistakes that end up introducing bugs. A lot of the time, these bugs are hard to track down and debug, and sometimes are even harder to replicate.
+This risk is made ever more real because of the generally lax nature of Lua. Incorrect code is regularly passed off and isn't noticed until something breaks at runtime. Sometimes you'll get a clear error message, and will have to spend time going back, fixing the code, and making sure you actually fixed it. Other times, the effects are more hidden, and instead of getting an error your code will just pass through along, in an incorrect state.
+Take, for example, this code:
+function Player:SwapWeapons()
+ self.CurrentWeapon = self.SideWeapon
+ self.SideWeapon = self.CurrentWeapon
+end
+
+This is code that is technically correct, but is absolutely not what you wanted to write. However, because it is technically correct, Lua will do exactly what you tell it to do, and so...
+player:SwapWeapons()
Uh oh! After debugging this, you realize that you actually meant to write was...
+function Player:SwapWeapons()
+ self.CurrentWeapon, self.SideWeapon = self.SideWeapon, self.CurrentWeapon
+end
+
+If you were using selene, you would've been alerted right away that your original code looked like an almost_swapped
.
error[almost_swapped]: this looks like you are trying to swap `self.CurrentWeapon` and `self.SideWeapon`
+
+ ┌── fail.lua:4:5 ───
+ │
+ 4 │ ╭ self.CurrentWeapon = self.SideWeapon
+ 5 │ │ self.SideWeapon = self.CurrentWeapon
+ │ ╰────────────────────────────────────────^
+ │
+ = try: `self.CurrentWeapon, self.SideWeapon = self.SideWeapon, self.CurrentWeapon`
+
+Other bugs arise because of Lua's lack of typing. While it can feel freeing to developers to not have to specify types everywhere, it makes it easier to mess up and write broken code. For example, take the following code:
+for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
+
+This code is yet again technically correct, but not what we wanted to do. pairs
will take the first argument, GoldShop
, and ignore the rest. Worse, the shop
variable will now be the values of the contents of GoldShop
, not the shop itself. This can cause massive headaches, since although you're likely to get an error later down the line, it's more likely it'll be in the vein of "attempt to index a nil value items
" than something more helpful. If you used ipairs
instead of pairs
, your code inside might just not run, and won't produce an error at all.
Yet again, selene saves us.
+error[incorrect_standard_library_use]: standard library function `pairs` requires 1 parameters, 3 passed
+
+ ┌── fail.lua:1:16 ───
+ │
+ 1 │ for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ │
+
+This clues the developer into writing the code they meant to write:
+for _, shop in pairs({ GoldShop, ItemShop, MedicineShop }) do
+
+While it's nice to write code however you want to, issues can arise when you are working with other people, or plan on open sourcing your work for others to contribute to. It's best for everyone involved if they stuck to the same way of writing Lua.
+Consider this contrived example:
+call(1 / 0)
+
+The person who wrote this code might have known that 1 / 0
evaluates to math.huge
. However, anyone working on that code will likely see it and spend some time figuring out why they wrote the code that way.
If the developer was using selene, this code would be denied:
+warning[divide_by_zero]: dividing by zero is not allowed, use math.huge instead
+
+ ┌── fail.lua:1:6 ───
+ │
+ 1 │ call(1 / 0)
+ │ ^^^^^
+ │
+
+Furthermore, selene is meant to be easy for developers to add their own lints to. You could create your own lints for your team to prevent behavior that is non-idiomatic to the codebase. For example, let's say you're working on a Roblox codebase, and you don't want your developers using the data storage methods directly. You could create your own lint so that this code:
+local DataStoreService = game:GetService("DataStoreService")
+
+...creates a warning, discouraging its use. For more information on how to create your own lints, check out the contribution guide.
+ +selene is a command line tool designed to help write correct and idiomatic Lua code.
+selene and all its source code are licensed under the Mozilla Public License 2.0.
+When writing any code, it's very easy to make silly mistakes that end up introducing bugs. A lot of the time, these bugs are hard to track down and debug, and sometimes are even harder to replicate.
+This risk is made ever more real because of the generally lax nature of Lua. Incorrect code is regularly passed off and isn't noticed until something breaks at runtime. Sometimes you'll get a clear error message, and will have to spend time going back, fixing the code, and making sure you actually fixed it. Other times, the effects are more hidden, and instead of getting an error your code will just pass through along, in an incorrect state.
+Take, for example, this code:
+function Player:SwapWeapons()
+ self.CurrentWeapon = self.SideWeapon
+ self.SideWeapon = self.CurrentWeapon
+end
+
+This is code that is technically correct, but is absolutely not what you wanted to write. However, because it is technically correct, Lua will do exactly what you tell it to do, and so...
+player:SwapWeapons()
Uh oh! After debugging this, you realize that you actually meant to write was...
+function Player:SwapWeapons()
+ self.CurrentWeapon, self.SideWeapon = self.SideWeapon, self.CurrentWeapon
+end
+
+If you were using selene, you would've been alerted right away that your original code looked like an almost_swapped
.
error[almost_swapped]: this looks like you are trying to swap `self.CurrentWeapon` and `self.SideWeapon`
+
+ ┌── fail.lua:4:5 ───
+ │
+ 4 │ ╭ self.CurrentWeapon = self.SideWeapon
+ 5 │ │ self.SideWeapon = self.CurrentWeapon
+ │ ╰────────────────────────────────────────^
+ │
+ = try: `self.CurrentWeapon, self.SideWeapon = self.SideWeapon, self.CurrentWeapon`
+
+Other bugs arise because of Lua's lack of typing. While it can feel freeing to developers to not have to specify types everywhere, it makes it easier to mess up and write broken code. For example, take the following code:
+for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
+
+This code is yet again technically correct, but not what we wanted to do. pairs
will take the first argument, GoldShop
, and ignore the rest. Worse, the shop
variable will now be the values of the contents of GoldShop
, not the shop itself. This can cause massive headaches, since although you're likely to get an error later down the line, it's more likely it'll be in the vein of "attempt to index a nil value items
" than something more helpful. If you used ipairs
instead of pairs
, your code inside might just not run, and won't produce an error at all.
Yet again, selene saves us.
+error[incorrect_standard_library_use]: standard library function `pairs` requires 1 parameters, 3 passed
+
+ ┌── fail.lua:1:16 ───
+ │
+ 1 │ for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ │
+
+This clues the developer into writing the code they meant to write:
+for _, shop in pairs({ GoldShop, ItemShop, MedicineShop }) do
+
+While it's nice to write code however you want to, issues can arise when you are working with other people, or plan on open sourcing your work for others to contribute to. It's best for everyone involved if they stuck to the same way of writing Lua.
+Consider this contrived example:
+call(1 / 0)
+
+The person who wrote this code might have known that 1 / 0
evaluates to math.huge
. However, anyone working on that code will likely see it and spend some time figuring out why they wrote the code that way.
If the developer was using selene, this code would be denied:
+warning[divide_by_zero]: dividing by zero is not allowed, use math.huge instead
+
+ ┌── fail.lua:1:6 ───
+ │
+ 1 │ call(1 / 0)
+ │ ^^^^^
+ │
+
+Furthermore, selene is meant to be easy for developers to add their own lints to. You could create your own lints for your team to prevent behavior that is non-idiomatic to the codebase. For example, let's say you're working on a Roblox codebase, and you don't want your developers using the data storage methods directly. You could create your own lint so that this code:
+local DataStoreService = game:GetService("DataStoreService")
+
+...creates a warning, discouraging its use. For more information on how to create your own lints, check out the contribution guide.
+selene is not the first Lua linter. The main inspiration behind selene is luacheck. However, the two have very little in common besides inception.
+selene:
+error[suspicious_reverse_loop]: this loop will only ever run once at most
+
+ ┌── fail.lua:1:9 ───
+ │
+ 1 │ for _ = #x, 1 do
+ │ ^^^^^
+ │
+ = help: try adding `, -1` after `1`
+
+luacheck:
+Checking fail.lua 2 warnings
+
+ fail.lua:1:1: numeric for loop goes from #(expr) down to 1 but loop step is not negative
+
+.luacheckrc
, which runs Lua.for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
+
+math.pi()
+
+...while luacheck does not.
+211
", while in selene you ignore "unbalanced_assignments
".-q
option (quiet).This is not to say selene is objectively better than luacheck, at least not yet.
+luacheck does not require much configuration to begin with, so migration should be easy.
+generate-roblox-std
? Should generate-roblox-std
be uploaded to crates.io?selene is mostly intended for use as a command line tool.
+In this section, you will learn how to use selene in this manner.
+selene is written in Rust, and the recommended installation method is through the Cargo package manager.
+To use Cargo, you must first install Rust. rustup.rs is a tool that makes this very easy.
+Once you have Rust installed, use either command:
+If you want the most stable version of selene
+cargo install selene
+
+(Note: Currently, the above method will give you an outdated selene, as the selene crate on Cargo is not yet maintained.)
+If you want the most up to date version of selene
+cargo install --git https://github.com/Kampfkarren/selene selene
+
+selene is built with Roblox specific lints by default. If you don't want these, type --no-default-features
after whichever command you choose.
If you want to get a quick understanding of the interface, simply type selene --help
.
USAGE:
+ selene [FLAGS] [OPTIONS] <files>...
+
+FLAGS:
+ -h, --help Prints help information
+ -q Display only the necessary information
+ -V, --version Prints version information
+
+OPTIONS:
+ --config <config> A toml file to configure the behavior of selene [default: selene.toml]
+ --num-threads <num-threads> Number of threads to run on, default to the numbers of logical cores on your
+ system [default: 8]
+ --pattern <pattern> A glob to match files with to check [default: **/*.lua]
+
+ARGS:
+ <files>...
+
+All unnamed inputs you give to selene will be treated as files to check for.
+If you want to check a folder of files: selene files
If you just want to check one file: selene code.lua
If you want to check multiple files/folders: selene file1 file2 file3 ...
-q
+--quiet
+Instead of the rich format, only necessary information will be displayed.
+~# selene code.lua
+warning[divide_by_zero]: dividing by zero is not allowed, use math.huge instead
+
+ ┌── code.lua:1:6 ───
+ │
+ 1 │ call(1 / 0)
+ │ ^^^^^
+ │
+
+Results:
+0 errors
+1 warnings
+0 parse errors
+
+~# selene code.lua -q
+code.lua:1:6: warning[divide_by_zero]: dividing by zero is not allowed, use math.huge instead
+
+Results:
+0 errors
+1 warnings
+0 parse errors
+
+--num-threads num-threads
+Specifies the number of threads for selene to use. Defaults to however many cores your CPU has. If you type selene --help
, you can see this number because it will show as the default for you.
--pattern pattern
+A glob 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".
In this section, you will learn how to interact with selene from your code and how to fit it to your liking.
+selene is meant to be easily configurable. You can specify configurations for the entire project as well as for individual lints.
+Configuration files are placed in the directory you are running selene in and are named selene.toml. As the name suggests, the configurations use the Tom's Obvious, Minimal Language (TOML) format. It is recommended you quickly brush up on the syntax, though it is very easy.
+You can change the severity of lints by entering the following into selene.toml:
+[rules]
+lint_1 = "severity"
+lint_2 = "severity"
+...
+
+Where "severity" is one of the following:
+"allow"
- Don't check for this lint"warn"
- Warn for this lint"deny"
- Error for this lintNote that "deny" and "warn" are effectively the same, only warn will give orange text while error gives red text, and they both have different counters.
+You can configure specific rules by entering the following into selene.toml:
+[config]
+rule1 = ...
+rule2 = ...
+...
+
+Where the value is whatever the special configuration of that rule is. You can learn these on the lints specific page in the list of lints. For example, if we wanted to allow empty if branches if the contents contain comments, then we would write:
+[config]
+empty_if = { comments_count = true }
+
+Many lints use standard libraries for either verifying their correct usage or for knowing that variables exist where they otherwise wouldn't.
+By default, selene uses Lua 5.1, though if we wanted to use the Lua 5.2 standard library, we would write:
+std = "lua52"
+
+...at the top of selene.toml. You can learn more about the standard library format on the standard library guide. The standard library given can either be one of the builtin ones (currently only lua51
and lua52
) or the filename of a standard library file in this format. For example, if we had a file named special.toml
, we would write:
std = "special"
+
+We can chain together multiple standard libraries by simply using a plus sign (+
) in between the names.
For example, if we had game.toml
and engine.toml
standard libraries, we could chain them together like so:
std = "game+engine"
+
+Lints can be toggled on and off in the middle of code when necessary through the use of special comments.
+Suppose we have the following code:
+local something = 1
+
+selene will correctly attribute this as an unused variable:
+warning[unused_variable]: something is assigned a value, but never used
+
+ ┌── code.lua:1:7 ───
+ │
+ 1 │ local something = 1
+ │ ^^^^^^^^^
+ │
+
+However, perhaps we as the programmer have some reason for leaving this unused (and not renaming it to _something
). This would be where inline lint filtering comes into play. In this case, we would simply write:
-- selene: allow(unused_variable)
+local something = 1
+
+This also works with settings other than allow
--you can warn
or deny
lints in the same fashion. For example, you can have a project with the following selene.toml
configuration:
[rules]
+unused_variable = "allow" # I'm fine with unused variables in code
+
+...and have this in a separate file:
+-- I'm usually okay with unused variables, but not this one
+-- selene: deny(unused_variable)
+local something = 1
+
+This is applied to the entire piece of code its near, not just the next line. For example:
+-- selene: allow(unused_variable)
+do
+ local foo = 1
+ local bar = 2
+end
+
+...will silence the unused variable warning for both foo
and bar
.
If you want to allow/deny a lint for an entire file, you can do this by attaching the following code to the beginning:
+--# selene: allow(lint_name)
+
+The #
tells selene that you want to apply these globally.
These must be before any code, otherwise selene will deny it. For example, the following code:
+local x = 1
+--# selene: allow(unused_variable)
+
+...will cause selene to error:
+warning[unused_variable]: x is assigned a value, but never used
+ ┌─ -:1:7
+ │
+1 │ local x = 1
+ │ ^
+
+error[invalid_lint_filter]: global filters must come before any code
+ ┌─ -:1:1
+ │
+1 │ local x = 1
+ │ ----------- global filter must be before this
+2 │ --# selene: allow(unused_variable)
+
+You can filter multiple lints in two ways:
+-- selene: allow(lint_one)
+-- selene: allow(lint_two)
+
+-- or...
+
+-- selene: allow(lint_one, lint_two)
+
+selene provides a robust standard library format to allow for use with environments other than vanilla Lua. Standard libraries are defined in the form of TOML files.
+For examples of the standard library format, see:
+lua51.toml
- The default standard library for Lua 5.1lua52.toml
- A standard library for Lua 5.2's additions and removals. Reference this if your standard library is based off another (it most likely is).roblox.toml
- A standard library for Roblox that incorporates all the advanced features of the format. If you are a Roblox developer, don't use this as anything other than reference--an up to date version of this library is available with every commit.Anything under the key [selene]
is used for meta information. The following paths are accepted:
[selene.base]
- Used for specifying what standard library to be based off of. Currently only accepts built in standard libraries, meaning lua51
or lua52
.
[selene.name]
- Used for specifying the name of the standard library. Used internally for cases such as only giving Roblox lints if the standard library is named "roblox"
.
[selene.structs]
- Used for declaring structs.
This is where the magic happens. The globals
field is a dictionary where the keys are the globals you want to define. The value you give tells selene what the value can be, do, and provide.
If your standard library is based off another, overriding something defined there will use your implementation over the original.
+Example:
+[foo]
+any = true
+
+Specifies that the field can be used in any possible way, meaning that foo.x
, foo:y()
, etc will all validate.
Example:
+[[tonumber.args]]
+type = "any"
+
+[[tonumber.args]]
+type = "number"
+required = false
+
+A field is a function if it contains an args
and/or method
field.
If method
is specified as true
and the function is inside a table, then it will require the function be called in the form of Table:FunctionName()
, instead of Table.FunctionName()
.
args
is an array of arguments, in order of how they're used in the function. An argument is in the form of:
required?: false | true | string;
+type: "any" | "bool" | "function" | "nil"
+ | "number" | "string" | "table" | "..."
+ | string[] | { "display": string }
+
+true
- The default, this argument is required.false
- This argument is optional."any"
- Allows any value."bool"
, "function"
, "nil"
, "number"
, "string"
, "table"
- Expects a value of the respective type."..."
- Allows any number of variables after this one. If required
is true (it is by default), then this will lint if no additional arguments are given. It is incorrect to have this in the middle.collectgarbage
only takes one of a few exact string arguments--doing collectgarbage("count")
will work, but collectgarbage("whoops")
won't.{ "display": string }
- Used when no constant could possibly be correct. If a constant is used, selene will tell the user that an argument of the type (display) is required. For an example, the Roblox method Color3.toHSV
expects a Color3
object--no constant inside it could be correct, so this is defined as:[[Color3.toHSV.args]]
+type = { display = "Color3" }
+
+Example:
+[_VERSION]
+property = true
+
+Specifies that a property exists. For example, _VERSION
is available as a global and doesn't have any fields of its own, so it is just defined as a property.
The same goes for _G
, which is defined as:
[_G]
+property = true
+writable = "new-fields"
+
+writable
is an optional field that tells selene how the property can be mutated and used:
"new-fields"
- New fields can be added and set, but variable itself cannot be redefined. In the case of _G, it means that _G = "foo"
is linted against."overridden"
- New fields can't be added, but entire variable can be overridden. In the case of Roblox's Instance.Name
, it means we can do Instance.Name = "Hello"
, but not Instance.Name.Call()
."full"
- New fields can be added and entire variable can be overridden.If writable
is not specified, selene will assume it can neither have new fields associated with it nor can be overridden.
Example:
+[game]
+struct = "DataModel"
+
+Specifies that the field is an instance of a struct. The value is the name of the struct.
+Example:
+[math.huge]
+property = true
+
+[math.pi]
+property = true
+
+A field is understood as a table if it has fields of its own. Notice that [math]
is not defined anywhere, but its fields are. Fields are of the same type as globals.
Example:
+[getfenv]
+removed = true
+
+Used when your standard library is based off another, and your library removes something from the original.
+Structs are used in places such as Roblox Instances. Every Instance in Roblox, for example, declares a :GetChildren()
method. We don't want to have to define this everywhere an Instance is declared globally, so instead we just define it once in a struct.
Structs are defined as fields of [selene.structs]
. Any fields they have will be used for instances of that struct. For example, the Roblox standard library has the struct:
[selene.structs.Event.Connect]
+method = true
+
+[[selene.structs.Event.Connect.args]]
+type = "function"
+
+From there, it can define:
+[workspace.Changed]
+struct = "Event"
+
+...and selene will know that workspace.Changed:Connect(callback)
is valid, but workspace.Changed:RandomNameHere()
is not.
Fields can specify requirements if a field is referenced that is not explicitly named. For example, in Roblox, instances can have arbitrary fields of other instances (workspace.Baseplate
indexes an instance named Baseplate inside workspace
, but Baseplate
is nowhere in the Roblox API).
We can specify this behavior by using the special "*"
field.
[workspace."*"]
+struct = "Instance"
+
+This will tell selene "any field accessed from workspace
that doesn't exist must be an Instance struct".
Wildcards can even be used in succession. For example, consider the following:
+[script.Name]
+property = true
+writable = "overridden"
+
+[script."*"."*"]
+property = true
+writable = "full"
+
+Ignoring the wildcard, so far this means:
+script.Name = "Hello"
will work.script = nil
will not work, because the writability of script
is not specified.script.Name.UhOh
will not work, because script.Name
does not have fields.However, with the wildcard, this adds extra meaning:
+script.Foo = 3
will not work, because the writability of script.*
is not specified.script.Foo.Bar = 3
will work, because script.*.*
has full writability.script.Foo.Bar.Baz = 3
will work for the same reason as above.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 in order to fix these issues, as well as get Roblox specific lints.
Thankfully, this process is very simple. All you need to do is edit your selene.toml
(or create one) and add the following:
std = "roblox"
The next time you run selene, a Roblox standard library will be automatically generated. and used.
+You can also initiate this process manually with selene generate-roblox-std
.
Deprecated event members are not added by default. This means code such as workspace.ChildAdded:connect(...)
will error. If you don't want to lint these, use selene generate-roblox-std --deprecated
.
selene is written in Rust, so knowledge of the ecosystem is expected.
+selene uses Full Moon to parse the Lua code losslessly, meaning whitespace and comments are preserved. You can read the full documentation for Full Moon on its docs.rs page.
+TODO: Upload selene-lib on crates.io and link the docs.rs page for that as well as throughout the rest of this article.
+In selene, lints are created in isolated modules. To start, create a file in selene-lib/src/rules
with the name of your lint. In this example, we're going to call the lint cool_lint.rs
.
Let's now understand what a lint consists of. selene takes lints in the form of structs that implement the Rule
trait. The Rule
trait expects:
Config
associated type that defines what the configuration format is expected to be. Whatever you pass must be deserializable.Error
associated type that implements std::error::Error
. This is used if configurations can be invalid (such as a parameter only being a number within a range). Most of the time, configurations cannot be invalid (other than deserializing errors, which are handled by selene), and so you can set this to std::convert::Infallible
.new
function with the signature fn new(config: Self::Config) -> Result<Self, Self::Error>
. With the selene CLI, this is called once.pass
function with the signature fn pass(&self, ast: &full_moon::ast::Ast<'static>, context: &Context) -> Vec<Diagnostic>
. The ast
argument is the Full Moon representation of the code, and the context provides optional additional information, such as the standard library being used. Any Diagnostic
structs returned here are displayed to the user.severity
function with the signature fn severity(&self) -> Severity
. Returns either Severity::Error
or Severity::Warning
. Use Error
if the code is positively impossible to be correct. The &self
is only provided due to limitations of Rust--the function should be completely constant and pure.rule_type
function with the signature fn rule_type(&self) -> RuleType
. Returns either Complexity
, Correctness
, Performance
, or Style
. So far not used for anything. Has the same gotcha as severity
in relation to &self
.For our purposes, we're going to write:
+use super::*;
+use std::convert::Infallible;
+
+struct CoolLint;
+
+impl Rule for CoolLint {
+ type Config = ();
+ type Error = Infallible;
+
+ fn new(_: Self::Config) -> Result<Self, Self::Error> {
+ Ok(CoolLint)
+ }
+
+ fn pass(&self, ast: &Ast, _: &Context) -> Vec<Diagnostic> {
+ unimplemented!()
+ }
+
+ fn severity(&self) -> Severity {
+ Severity::Warning
+ }
+
+ fn rule_type(&self) -> RuleType {
+ RuleType::Style
+ }
+}
+
+The implementation of pass
is completely up to you, but there are a few common patterns.
divide_by_zero
and suspicious_reverse_loop
for straight forward examples.ScopeManager
struct to lint based off of usage of variables and references. See shadowing
and global_usage
.Now that we have our lint, we have to make sure selene actually knows to use it. There are two places you need to update.
+In selene-lib/src/lib.rs, search for use_rules!
. You will see something such as:
use_rules! {
+ almost_swapped: rules::almost_swapped::AlmostSwappedLint,
+ divide_by_zero: rules::divide_by_zero::DivideByZeroLint,
+ empty_if: rules::empty_if::EmptyIfLint,
+ ...
+}
+
+Put your lint in this list (alphabetical order) as the following format:
+lint_name: rules::module_name::LintObject,
+
+For us, this would be:
+cool_lint: rules::cool_lint::CoolLint,
+
+Next, in selene-lib/src/rules.rs
, search for pub mod
, and you will see:
pub mod almost_swapped;
+pub mod divide_by_zero;
+pub mod empty_if;
+...
+
+Put your module name in this list, also in alphabetical order.
+pub mod almost_swapped;
+pub mod cool_lint;
+pub mod divide_by_zero;
+pub mod empty_if;
+...
+
+And we're done! You should be able to cargo build --bin selene
and be able to use your new lint.
The selene codebase uses tests extensively for lints. It means we never have to actually build the CLI tool in order to test, and we can make sure we don't have any regressions. Testing is required if you want to submit your lint to the selene codebase.
+To write a simple test, create a folder in selene-lib/tests
with the name of your lint. Then, create as many .lua
files as you want to test. These should contain both cases that do and do not lint. For our cases, we're going to assume our test is called cool_lint.lua
.
Then, in your lint module, add at the bottom:
+#[cfg(test)]
+mod tests {
+ use super::{super::test_util::test_lint, *};
+
+ #[test]
+ fn test_cool_lint() {
+ test_lint(
+ CoolLint::new(()).unwrap(),
+ "cool_lint",
+ "cool_lint",
+ );
+ }
+}
+
+Let's discuss what this code means, assuming you're familiar with the way tests are written and performed in Rust.
+The test_lint
function is the easiest way to test that a lint works. It'll search the source code we made before, run selene on it (only your lint), and check its results with the existing [filename].stderr
file, or create one if it does not yet exist.
The first argument is the lint object to use. CoolLint::new(())
just means "create CoolLint
with a configuration of ()
". If your lint specifies a configuration, this will instead be something such as CoolLintConfig::default()
or whatever you're specifically testing.
The .unwrap()
is just because CoolLint::new
returns a Result
. If you want to test configuration errors, you can avoid test_lint
altogether and just test CoolLint::new(...).is_err()
directly.
The first "cool_lint"
is the name of the folder we created. The second "cool_lint"
is the name of the Lua file we created.
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 rule, 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 to use. If you do not, the Lua 5.1 standard library will be used.
This step is only if you are contributing to the selene codebase, and not just writing personal lints (though I'm sure your other programmers would love if you did this).
+To document a new lint, edit docs/src/SUMMARY.md
, and add your lint to the table of contents along the rest. As with everything else, make sure it's in alphabetical order.
Then, edit the markdown file it creates (if you have mdbook serve
on, it'll create it for you), and write it in this format:
# rule_name
+## What it does
+Explain what your lint does, simply.
+
+## Why this is bad
+Explain why a user would want to lint this.
+
+## Configuration
+Specify any configuration if it exists.
+
+## Example
+```lua
+-- Bad code here
+```
+
+...should be written as...
+
+```lua
+-- Good code here
+```
+
+## Remarks
+If there's anything else a user should know when using this lint, write it here.
+
+This isn't a strict format, and you can mess with it as appropriate. For example, standard_library
does not have a "Why this is bad" section as not only is it a very encompassing rule, but it should be fairly obvious. Many rules don't specify a "...should be written as..." as it is either something with various potential fixes (such as global_usage
) or because the "good code" is just removing parts entirely (such as unbalanced_assignments
).
The following is the list of lints that selene will check for in your code.
+Checks for foo = bar; bar = foo
sequences.
This looks like a failed attempt to swap.
+a = b
+b = a
+
+...should be written as...
+a, b = b, a
+
+Checks for division by zero. Allows 0 / 0
as a way to get nan.
n / 0
equals math.huge
when n is positive, and -math.huge
when n is negative. Use these values directly instead, as using the / 0
way is confusing to read and non-idiomatic.
print(1 / 0)
+print(-1 / 0)
+
+...should be written as...
+print(math.huge)
+print(-math.huge)
+
+Checks for empty if blocks.
+You most likely forgot to write code in there or commented it out without commenting out the if statement itself.
+comments_count
(default: false
) - A bool that determines whether or not if statements with exclusively comments are empty.
-- Each of these branches count as an empty if.
+if a then
+elseif b then
+else
+end
+
+if a then
+ -- If comments_count is true, this will not count as empty.
+end
+
+Prohibits use of _G
.
_G
is global mutable state, which is heavily regarded as harmful. You should instead refactor your code to be more modular in nature.
If you are using the Roblox standard library, use of shared
is prohibited under this rule.
_G.foo = 1
+
+Checks for branches in if blocks that are equivalent.
+This is most likely a copy and paste error.
+if foo then
+ print(1)
+else
+ print(1)
+end
+
+Checks for branches in if blocks with equivalent conditions.
+This is most likely a copy and paste error.
+if foo then
+ print(1)
+elseif foo then
+ print(1)
+end
+
+This ignores conditions that could have side effects, such as function calls. This will not lint:
+if foo() then
+ print(1)
+elseif foo() then
+ print(1)
+end
+
+...as the result of foo()
could be different the second time it is called.
Checks for multiple statements on the same line.
+This can make your code difficult to read.
+one_line_if
(default: "break-return-only"
) - Defines whether or not one line if statements should be allowed. One of three options:
if x then return end
or if x then break end
is ok, but if x then call() end
is not.foo() bar() baz()
+
+...should be written as...
+foo()
+bar()
+baz()
+
+Checks for conditions in the form of (expression)
.
Lua does not require these, and they are not idiomatic.
+if (x) then
+repeat until (x)
+while (x) do
+
+...should be written as...
+if x then
+repeat until x
+while x do
+
+Checks for uses of Color3.new
where the arguments are not between 0 and 1.
Most likely, you are trying to use values of 0 to 255. This will not give you an error, and will silently give you the wrong color. You probably meant to use Color3.fromRGB
instead.
Color3.new(255, 0, 0)
+
+This lint is only active if you are using the Roblox standard library.
+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.
+This is guaranteed to fail once it is rendered. Furthermore, the createElement itself will not error--only once its mounted will it error.
+Roact.createElement("Frame", {
+ ThisPropertyDoesntExist = true,
+})
+
+Roact.createElement("BadClass", {})
+
+This lint is naive and makes several assumptions about the way you write your code. The assumptions are based on idiomatic Roact.
+Roact.createElement
directly or creating a local variable that's assigned to Roact.createElement
.undefined_variable
will still lint, however.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.
Additionally, this lint is based off of rbx_reflection. In practice, this means that if Roblox adds new properties or classes, selene will not know they exist until you update it.
+This lint is only active if you are using the Roblox standard library.
+Checks for overriding of variables under the same name.
+This can cause confusion when reading the code when trying to understand which variable is being used, and if you want to use the original variable you either have to redefine it under a temporary name or refactor the code that shadowed it.
+ignore_pattern
(default: "^_"
) - A regular expression that is used to specify names that are allowed to be shadowed. The default allows for variables like _
to be shadowed, as they shouldn't be used anyway.
local x = 1
+
+if foo then
+ local x = 1
+end
+
+Checks for correct use of the standard library.
+for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
+
+It is highly recommended that you do not turn this lint off. If you are having standard library issues, modify your standard library instead to be correct. If it is a problem with an official standard library (Ex: the Lua 5.1 or Roblox ones), you can file an issue on GitHub.
+Checks for for _ = #x, 1 do
sequences without specifying a negative step.
This loop will only run at most once, instead of going in reverse. If you truly did mean to run your loop only once, just use if #x > 0
instead.
for _ = #x, 1 do
+
+...should be written as...
+for _ = #x, 1, -1 do
+
+Checks for type(foo == "type")
, instead of type(foo) == "type"
.
This will always return "boolean"
, and is undoubtedly not what you intended to write.
return type(foo == "number")
+
+...should be written as...
+return type(foo) == "number"
+
+When using the Roblox standard library, this checks typeof
as well.
Checks for unbalanced assignments, such as a, b, c = 1
.
You shouldn't declare variables you're not immediately initializing on the same line as ones you are. This is most likely just forgetting to specify the rest of the variables.
+a, b, c = 1
+a = 1, 2
+
+There are a few things this lint won't catch.
+a, b, c = call()
will not lint, as call()
could return multiple values.
a, b, c = call(), 2
will, however, as you will only be using the first value of call()
. You will even receive a helpful message about this.
error[unbalanced_assignments]: values on right side don't match up to the left side of the assignment
+
+ ┌── unbalanced_assignments.lua:6:11 ───
+ │
+ 6 │ a, b, c = call(), 2
+ │ ^^^^^^^^^
+ │
+
+ ┌── unbalanced_assignments.lua:6:11 ───
+ │
+ 6 │ a, b, c = call(), 2
+ │ ------ help: if this function returns more than one value, the only first return value is actually used
+ │
+
+If nil is specified as the last value, the rest will be ignored. This means...
+a, b, c = nil
+
+...will not lint.
+Checks for uses of variables that are not defined.
+This is most likely a typo.
+-- vv oops!
+prinnt("hello, world!")
+
+If you are using a different standard library where a global variable is defined that selene isn't picking up on, create a standard library that specifies it.
+Checks for variables that are unscoped (don't have a local variable attached).
+Unscoped variables make code harder to read and debug, as well as making it harder for selene to analyze.
+ignore_pattern
(default: "^_"
) - A regular expression for variables that are allowed to be unscoped. The default allows for variables like _
to be unscoped, as they shouldn't be used anyway.
baz = 3
+
+Checks for variables that are unused.
+The existence of unused variables could indicate buggy code.
+allow_unused_self
(default: true
) - A bool that determines whether not using self
in a method function (function Player:SwapWeapons()
) is allowed.
ignore_pattern
(default: "^_"
) - A regular expression for variables that are allowed to be unused. The default allows for variables like _
to be unused, as they shouldn't be used anyway.
local foo = 1
+
+If you intend to create a variable without using it, replace it with _
or something that starts with _
. You'll see this most in generic for loops.
for _, value in ipairs(list) do
+
+
+ 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 in order to fix these issues, as well as get Roblox specific lints.
Thankfully, this process is very simple. All you need to do is edit your selene.toml
(or create one) and add the following:
std = "roblox"
The next time you run selene, a Roblox standard library will be automatically generated. and used.
+You can also initiate this process manually with selene generate-roblox-std
.
Deprecated event members are not added by default. This means code such as workspace.ChildAdded:connect(...)
will error. If you don't want to lint these, use selene generate-roblox-std --deprecated
.
selene is a command line tool designed to help write correct and idiomatic Lua code.
+selene and all its source code are licensed under the Mozilla Public License 2.0.
+ +selene is meant to be easily configurable. You can specify configurations for the entire project as well as for individual lints.
+Configuration files are placed in the directory you are running selene in and are named selene.toml. As the name suggests, the configurations use the Tom's Obvious, Minimal Language (TOML) format. It is recommended you quickly brush up on the syntax, though it is very easy.
+You can change the severity of lints by entering the following into selene.toml:
+[rules]
+lint_1 = "severity"
+lint_2 = "severity"
+...
+
+Where "severity" is one of the following:
+"allow"
- Don't check for this lint"warn"
- Warn for this lint"deny"
- Error for this lintNote that "deny" and "warn" are effectively the same, only warn will give orange text while error gives red text, and they both have different counters.
+You can configure specific rules by entering the following into selene.toml:
+[config]
+rule1 = ...
+rule2 = ...
+...
+
+Where the value is whatever the special configuration of that rule is. You can learn these on the lints specific page in the list of lints. For example, if we wanted to allow empty if branches if the contents contain comments, then we would write:
+[config]
+empty_if = { comments_count = true }
+
+Many lints use standard libraries for either verifying their correct usage or for knowing that variables exist where they otherwise wouldn't.
+By default, selene uses Lua 5.1, though if we wanted to use the Lua 5.2 standard library, we would write:
+std = "lua52"
+
+...at the top of selene.toml. You can learn more about the standard library format on the standard library guide. The standard library given can either be one of the builtin ones (currently only lua51
and lua52
) or the filename of a standard library file in this format. For example, if we had a file named special.toml
, we would write:
std = "special"
+
+We can chain together multiple standard libraries by simply using a plus sign (+
) in between the names.
For example, if we had game.toml
and engine.toml
standard libraries, we could chain them together like so:
std = "game+engine"
+
+
+ Lints can be toggled on and off in the middle of code when necessary through the use of special comments.
+Suppose we have the following code:
+local something = 1
+
+selene will correctly attribute this as an unused variable:
+warning[unused_variable]: something is assigned a value, but never used
+
+ ┌── code.lua:1:7 ───
+ │
+ 1 │ local something = 1
+ │ ^^^^^^^^^
+ │
+
+However, perhaps we as the programmer have some reason for leaving this unused (and not renaming it to _something
). This would be where inline lint filtering comes into play. In this case, we would simply write:
-- selene: allow(unused_variable)
+local something = 1
+
+This also works with settings other than allow
--you can warn
or deny
lints in the same fashion. For example, you can have a project with the following selene.toml
configuration:
[rules]
+unused_variable = "allow" # I'm fine with unused variables in code
+
+...and have this in a separate file:
+-- I'm usually okay with unused variables, but not this one
+-- selene: deny(unused_variable)
+local something = 1
+
+This is applied to the entire piece of code its near, not just the next line. For example:
+-- selene: allow(unused_variable)
+do
+ local foo = 1
+ local bar = 2
+end
+
+...will silence the unused variable warning for both foo
and bar
.
If you want to allow/deny a lint for an entire file, you can do this by attaching the following code to the beginning:
+--# selene: allow(lint_name)
+
+The #
tells selene that you want to apply these globally.
These must be before any code, otherwise selene will deny it. For example, the following code:
+local x = 1
+--# selene: allow(unused_variable)
+
+...will cause selene to error:
+warning[unused_variable]: x is assigned a value, but never used
+ ┌─ -:1:7
+ │
+1 │ local x = 1
+ │ ^
+
+error[invalid_lint_filter]: global filters must come before any code
+ ┌─ -:1:1
+ │
+1 │ local x = 1
+ │ ----------- global filter must be before this
+2 │ --# selene: allow(unused_variable)
+
+You can filter multiple lints in two ways:
+-- selene: allow(lint_one)
+-- selene: allow(lint_two)
+
+-- or...
+
+-- selene: allow(lint_one, lint_two)
+
+
+ In this section, you will learn how to interact with selene from your code and how to fit it to your liking.
+ +selene provides a robust standard library format to allow for use with environments other than vanilla Lua. Standard libraries are defined in the form of TOML files.
+For examples of the standard library format, see:
+lua51.toml
- The default standard library for Lua 5.1lua52.toml
- A standard library for Lua 5.2's additions and removals. Reference this if your standard library is based off another (it most likely is).roblox.toml
- A standard library for Roblox that incorporates all the advanced features of the format. If you are a Roblox developer, don't use this as anything other than reference--an up to date version of this library is available with every commit.Anything under the key [selene]
is used for meta information. The following paths are accepted:
[selene.base]
- Used for specifying what standard library to be based off of. Currently only accepts built in standard libraries, meaning lua51
or lua52
.
[selene.name]
- Used for specifying the name of the standard library. Used internally for cases such as only giving Roblox lints if the standard library is named "roblox"
.
[selene.structs]
- Used for declaring structs.
This is where the magic happens. The globals
field is a dictionary where the keys are the globals you want to define. The value you give tells selene what the value can be, do, and provide.
If your standard library is based off another, overriding something defined there will use your implementation over the original.
+Example:
+[foo]
+any = true
+
+Specifies that the field can be used in any possible way, meaning that foo.x
, foo:y()
, etc will all validate.
Example:
+[[tonumber.args]]
+type = "any"
+
+[[tonumber.args]]
+type = "number"
+required = false
+
+A field is a function if it contains an args
and/or method
field.
If method
is specified as true
and the function is inside a table, then it will require the function be called in the form of Table:FunctionName()
, instead of Table.FunctionName()
.
args
is an array of arguments, in order of how they're used in the function. An argument is in the form of:
required?: false | true | string;
+type: "any" | "bool" | "function" | "nil"
+ | "number" | "string" | "table" | "..."
+ | string[] | { "display": string }
+
+true
- The default, this argument is required.false
- This argument is optional."any"
- Allows any value."bool"
, "function"
, "nil"
, "number"
, "string"
, "table"
- Expects a value of the respective type."..."
- Allows any number of variables after this one. If required
is true (it is by default), then this will lint if no additional arguments are given. It is incorrect to have this in the middle.collectgarbage
only takes one of a few exact string arguments--doing collectgarbage("count")
will work, but collectgarbage("whoops")
won't.{ "display": string }
- Used when no constant could possibly be correct. If a constant is used, selene will tell the user that an argument of the type (display) is required. For an example, the Roblox method Color3.toHSV
expects a Color3
object--no constant inside it could be correct, so this is defined as:[[Color3.toHSV.args]]
+type = { display = "Color3" }
+
+Example:
+[_VERSION]
+property = true
+
+Specifies that a property exists. For example, _VERSION
is available as a global and doesn't have any fields of its own, so it is just defined as a property.
The same goes for _G
, which is defined as:
[_G]
+property = true
+writable = "new-fields"
+
+writable
is an optional field that tells selene how the property can be mutated and used:
"new-fields"
- New fields can be added and set, but variable itself cannot be redefined. In the case of _G, it means that _G = "foo"
is linted against."overridden"
- New fields can't be added, but entire variable can be overridden. In the case of Roblox's Instance.Name
, it means we can do Instance.Name = "Hello"
, but not Instance.Name.Call()
."full"
- New fields can be added and entire variable can be overridden.If writable
is not specified, selene will assume it can neither have new fields associated with it nor can be overridden.
Example:
+[game]
+struct = "DataModel"
+
+Specifies that the field is an instance of a struct. The value is the name of the struct.
+Example:
+[math.huge]
+property = true
+
+[math.pi]
+property = true
+
+A field is understood as a table if it has fields of its own. Notice that [math]
is not defined anywhere, but its fields are. Fields are of the same type as globals.
Example:
+[getfenv]
+removed = true
+
+Used when your standard library is based off another, and your library removes something from the original.
+Structs are used in places such as Roblox Instances. Every Instance in Roblox, for example, declares a :GetChildren()
method. We don't want to have to define this everywhere an Instance is declared globally, so instead we just define it once in a struct.
Structs are defined as fields of [selene.structs]
. Any fields they have will be used for instances of that struct. For example, the Roblox standard library has the struct:
[selene.structs.Event.Connect]
+method = true
+
+[[selene.structs.Event.Connect.args]]
+type = "function"
+
+From there, it can define:
+[workspace.Changed]
+struct = "Event"
+
+...and selene will know that workspace.Changed:Connect(callback)
is valid, but workspace.Changed:RandomNameHere()
is not.
Fields can specify requirements if a field is referenced that is not explicitly named. For example, in Roblox, instances can have arbitrary fields of other instances (workspace.Baseplate
indexes an instance named Baseplate inside workspace
, but Baseplate
is nowhere in the Roblox API).
We can specify this behavior by using the special "*"
field.
[workspace."*"]
+struct = "Instance"
+
+This will tell selene "any field accessed from workspace
that doesn't exist must be an Instance struct".
Wildcards can even be used in succession. For example, consider the following:
+[script.Name]
+property = true
+writable = "overridden"
+
+[script."*"."*"]
+property = true
+writable = "full"
+
+Ignoring the wildcard, so far this means:
+script.Name = "Hello"
will work.script = nil
will not work, because the writability of script
is not specified.script.Name.UhOh
will not work, because script.Name
does not have fields.However, with the wildcard, this adds extra meaning:
+script.Foo = 3
will not work, because the writability of script.*
is not specified.script.Foo.Bar = 3
will work, because script.*.*
has full writability.script.Foo.Bar.Baz = 3
will work for the same reason as above.