selene Lua linter

repository·main·Indexed 21 days ago

https://github.com/kampfkarren/selene

A high-performance, modern Lua linter written in Rust designed for diagnostic accuracy, extensibility, and minimal configuration. It includes a CLI for checking files and directories, a VS Code integration via selene-vscode, and support for Roblox-specific lints.

Tokens
26.6K
Snippets
121
Records
146
Agent score
73%

What's inside selene

  1. Overview of selene Lua linter

    main
    selene is a high-performance, modern Lua linter implemented in Rust. It is designed with a focus on diagnostic accuracy (preferring to miss a problem rather than report a false positive), extensibility, and ease of configuration. The tool aims to provide a high-quality linting experience with minimal user configuration required out of the box.
  2. Understand the `incorrect_standard_library_use` lint

    main

    The incorrect_standard_library_use lint checks whether you are using the standard library correctly according to Selene's definitions. It identifies calls to standard library functions that use incorrect arguments or patterns.

    Note: It is highly recommended that you do not disable this lint. If you encounter issues with the standard library, you should aim to correct your usage or modify your standard library definition to be accurate rather than turning the lint off.

    -- Example of code that might trigger this lint:
    for _, shop in pairs(GoldShop, ItemShop, MedicineShop) do
  3. How restricted_module_paths detects violations

    main

    The restricted_module_paths lint scans expression contexts to find restricted module paths. It is designed to catch usage in various scenarios, including:

    • Assignments: local x = Restricted.Path
    • Function calls: Restricted.Path()
    • Function arguments: fn(Restricted.Path)
    • Table constructors: { key = Restricted.Path }
    • Return statements: return Restricted.Path
    • Nested structures: { a = { b = Restricted.Path } }
    • Conditional expressions: condition and Restricted.Path or nil
    • Require statements: require(Restricted.Path)
    • Global assignments: global = Restricted.Path

    Limitations

    The lint does not check:

    • String require statements: require("Module.SubModule")
    • String literals: "Module.SubModule.function"

    It also uses exact string matching; for example, a restriction on A.B will not trigger for A.BExtended.

    -- Example of what triggers the lint:
    local deprecatedFunction = OldLibrary.Utils.deprecatedFunction
    OldLibrary.Utils.deprecatedFunction()
    fn(OldLibrary.Utils.deprecatedFunction)
    local config = { callback = OldLibrary.Utils.deprecatedFunction }
    
    function getHandler()
        return OldLibrary.Utils.deprecatedFunction
    end
    
    local nested = { deep = { handler = OldLibrary.Utils.deprecatedFunction } }
    local handler = condition and OldLibrary.Utils.deprecatedFunction or nil
    local required = require(OldLibrary.Utils.deprecatedFunction)
    global = OldLibrary.Utils.deprecatedFunction
  4. Gotcha: Using ipairs for table cloning

    main

    When using ipairs to clone a table, table.clone is not an exact functional match if the table contains non-array keys (mixed tables). ipairs only iterates over the array part of a table, whereas table.clone clones the entire table.

    If you use ipairs in a pattern that matches the manual clone lint, you will be notified of this potential discrepancy.

    Example of a non-equivalent clone:

    local mixedTable = { 1, 2, 3 }
    mixedTable.key = "value"
    
    local clone = {}
    
    -- This lints, but is NOT equivalent to table.clone because it misses 'key'
    for key, value in ipairs(mixedTable) do
        clone[key] = value
    end
    local mixedTable = { 1, 2, 3 }
    mixedTable.key = "value"
    
    local clone = {}
    
    -- Lints, but is not equivalent, since ipairs only loops over the array part.
    for key, value in ipairs(mixedTable) do
        clone[key] = value
    end
  5. Use Wildcards for dynamic fields

    main

    Wildcards (*) allow you to specify requirements for fields that are not explicitly named in your standard library. This is useful for environments where objects can have arbitrary child properties.

    • workspace.*: Any field accessed from workspace that isn't explicitly defined will be treated as a specific struct.
    • Successive wildcards like script.*.* allow you to define writability for deeply nested dynamic paths.
    # Any field accessed from workspace that doesn't exist must be an Instance struct
    workspace.*:
      struct: Instance
    
    # Deeply nested dynamic fields have full writability
    script.*.*:
      property: full-write
  6. Scope of the duplicate_keys lint rule

    main

    The duplicate_keys rule has specific limitations on what it detects:

    • Supported Keys: It only handles keys that are constant string/number literals or named keys (e.g., { a = true }).
    • Array-like Values: It handles array-like values by treating them as implicit integer keys. For example, {"foo"} is treated as { [1] = "foo" }.
  7. Compare selene vs. luacheck

    main

    If you are considering switching from luacheck to selene, consider these key differences:

    • Performance: selene is written in Rust and is multithreaded, making it significantly faster than the Lua-based luacheck.
    • Configuration: selene uses TOML files, whereas luacheck uses .luacheckrc (which executes Lua code).
    • Output: selene provides rich, actionable error messages with visual pointers and help suggestions. luacheck provides basic text warnings.
    • Linting Logic: selene supports advanced standard library configuration (argument types, counts, etc.), allowing it to catch errors like incorrect function calls (e.g., math.pi()) that luacheck misses.
    • Lint Management: selene uses descriptive English names for lints (e.g., unbalanced_assignments) instead of numeric codes. It also distinguishes between deny and warn severities.
    • Filtering: selene allows filtering specific lints and applies rules over code blocks rather than just individual lines.
    • Roblox Support: selene has optional support and a large focus specifically for Roblox development.

    What selene does NOT currently do (compared to luacheck):

    • Lint for long lines or whitespace issues (style issues).
    • Support Lua versions past 5.1.
    • Detect unreachable code, unused labels, variables that are only mutated but never read, or the use of uninitialized variables.
  8. Understand the global_usage lint

    main

    The global_usage lint is designed to prevent the use of _G, which represents global mutable state. Using _G is considered harmful because it makes code harder to reason about and less modular. Instead of using _G, you should refactor your code to be more modular.

    Key constraints:

    • Prohibits use of _G.
    • In the Roblox standard library, use of shared is also prohibited.
    • You can bypass this for specific names using ignore_pattern (a regex).
    -- This will trigger the global_usage lint:
    _G.foo = 1
  9. Understand the shadowing lint rule

    main

    The shadowing rule detects when a variable name is reused in a nested scope, effectively hiding the original variable. This is flagged because it can cause confusion when reading code and makes it difficult to access the original variable without refactoring or renaming.

    local x = 1
    
    if foo then
        local x = 1 -- This shadows the outer 'x'
    end
  10. Understand the mismatched_arg_count lint

    main

    The mismatched_arg_count lint identifies instances where more arguments are passed to a function call than are defined in that function's signature. This is used to catch unnecessary arguments that might indicate a misunderstanding of the function's API.

    Limitations:

    • Does not check for missing arguments: It does not flag when too few arguments are passed, as Lua often treats missing arguments as nil (e.g., foo(1) is often used intentionally instead of foo(1, nil)).
    • Static Analysis Constraint: Because Selene is a static analyzer and does not execute your code, it cannot track function reassignments that occur inside conditional logic or function calls. If a function is redefined at runtime, Selene will only lint based on the definitions it can statically determine.
    local function foo(a, b)
    end
    
    foo(1, 2, 3) -- error, function takes 2 arguments, but 3 were supplied
  11. Define global variables in the `globals` field

    main

    The globals field is a dictionary where keys are the global names. The value defines how Selene validates that global. You can define globals as:

    • Any: Use any: true to allow any usage (indexing, calling, etc.).
    • Functions: Defined by providing args and/or method.
    • Properties: Defined using the property key to specify writability.
    • Structs: Defined using the struct key to link to a named struct.
    • Tables: Implicitly created if a global has its own sub-fields defined.