Linting
Built-in static analysis to catch style issues and code smells without executing your code.
Usage
# Lint all .sl files in current directory (recursive)
soli lint
# Lint a specific directory
soli lint src/
# Lint a single file
soli lint app/main.sl
Exit codes
0 — no issues found. 1 — one or more issues found.
Output Format
Each issue is reported on a single line with the file path, line, column, rule, and message:
app/main.sl:12:5 - [naming/snake-case] variable 'myVar' should use snake_case
app/main.sl:30:9 - [smell/unreachable-code] unreachable code after return statement
2 issue(s)
found in 1 file(s)
Rules
Naming
naming/snake-case
Variables, functions, methods, and parameters should use snake_case.
# Bad
let myVar = 10
def processData end
# Good
let my_var = 10
def process_data end
naming/pascal-case
Classes and interfaces should use PascalCase.
# Bad
class my_class end
interface data_store end
# Good
class MyClass end
interface DataStore end
Style
style/empty-block
Blocks should not be empty. Add a comment or remove the block.
style/line-length
Lines should not exceed 120 characters.
Code Smells
smell/unreachable-code
Code after a return statement is unreachable and will never execute.
def example
return 42
print("never reached"); # Warning: unreachable code
end
smell/empty-catch
Catch blocks should not be empty. Silently swallowing errors hides bugs.
# Bad - error silently ignored
try
risky()catch e
end
# Good - at least log the error
try
risky()catch e
print("Error: " + str(e))
end
smell/deep-nesting
Nesting depth should not exceed 4 levels. Consider extracting logic into separate functions.
smell/duplicate-methods
A class should not have two methods with the same name.
smell/dangerous-server-builtin
Calls to db_query_raw, Trusted.*, System.shell / System.shell_sync, or backtick command substitution from app/controllers/, app/middleware/, or app/views/. These primitives are powerful but become injection / traversal sinks when fed request-controlled data. The diagnostic spells out the safe alternative for each:
db_query_raw→ parameterised@sdbql{ ... #{value} ... }block, orModel.where("x = #{v}", { "v": v }).Trusted.*→ jailedFile.*(read/write/exists), which keeps every operation under the app root.System.shell/ backticks →System.run(["prog", "arg1", ...])with an argv array, which never invokes a shell.
Models, migrations, and tests are out of scope — those layers legitimately use these APIs against operator-controlled data.
Idioms
These flag code that is correct but un-idiomatic — the Soli-specific patterns that read better with a builtin.
idiom/nil-comparison
Prefer .nil? / .present? over comparing to null.
# Bad
if user == null end
return user != null
# Good
if user.nil? end
return user.present?
idiom/prefer-blank
Prefer .blank? / .present? over comparing to an empty string. .blank? also covers the nil case.
# Bad
if name == "" end
# Good
if name.blank? end
idiom/prefer-includes
Replace a chain of 3+ comparisons of the same value with .includes?.
# Bad
status == "up" || status == "late" || status == "overdue"
# Good
["up", "late", "overdue"].includes?(status)
idiom/manual-find-guard
Model.find raises RecordNotFound on a miss (which the request handler turns into a 404), so a nil-check on its result never runs. Drop it, or use find_by / first_by when you want a nil result.
# Bad - dead code: .find already raised
post = Post.find(id)
return not_found() if post.nil?
# Good
post = Post.find(id)
component/props
A component's props(...) declaration must use string-literal names with no duplicates. Missing/unknown props are checked at render time in --dev, not by lint.
# Bad
props("title", "title") # duplicate
props("title", x) # not a string literal
# Good
props("title", "value")
Suppressing Warnings
When a warning is a known false-positive or an intentional exception, suppress it inline with a directive comment.
Single-line forms
disable-next-line covers the line below; disable-line covers the same line.
# soli-lint-disable-next-line smell/dangerous-server-builtin
if Trusted.is_dir(wt_path)
...
end
Trusted.read(p) # soli-lint-disable-line smell/dangerous-server-builtin
Block forms
disable / enable toggle a rule for a region. Useful when several adjacent lines are intentional exceptions.
# soli-lint-disable smell/dangerous-server-builtin
exists = Trusted.is_dir(path)
data = Trusted.read(path)
# soli-lint-enable smell/dangerous-server-builtin
- Omit the rule name to suppress every rule (e.g.
# soli-lint-disable). Pass a comma-separated list to scope to multiple rules. - An
enablefor a specific rule re-enables only that rule, even if the priordisablewas a blanket one. - A block
disablewith no matchingenableruns to the end of the file. - Prefer naming the exact rule so unrelated warnings still surface.
Editor Integration
The VS Code / Cursor extension provides full Language Server Protocol (LSP) support with real-time linting, hover documentation, autocomplete, and more.
Features
- Real-time linting — warnings and errors displayed inline
- Hover information — documentation for functions, classes, and builtins
- Autocomplete — suggestions for keywords, types, and symbols
- Go to definition — jump to symbol definitions
- Find references — locate all uses of a symbol
Installation
cd editors/vscode
vsce package
# Install the generated .vsix file in Cursor or VS Code
Settings
soli.lsp.enable— Enable/disable LSP server (default:true)soli.lsp.executablePath— Path to thesolibinary (default:"soli")soli.lint.enable— Enable/disable linting (default:true)soli.lint.onSave— Run linter on file save (default:true)
Manual LSP Setup
For editors that support custom LSP servers directly (Neovim, Emacs, etc.):
require('lspconfig').soli.setup({
cmd = {"soli", "lsp"},
filetypes = {"soli"},
root_dir = lspconfig.util.root_pattern("soli.toml", ".git"),
})
Learn More
For full editor setup instructions and all available LSP features, see the Editor Integration guide.
Type checking with soli check
Where soli lint catches style and smells, soli check runs Soli's optional type system over your code without executing it — ideal for CI or a pre-commit hook. It resolves imports and reports every mismatch with a file:line:column location, exiting non-zero when any are found.
soli check # type-check the current project
soli check app/models # check a directory
soli check app/models/user.sl
# example output
# app/models/user.sl: Type error: Type mismatch: expected Int, found String at 14:14
# 1 error(s) in 1 of 38 file(s)
Running a file with soli run type-checks it first too (use --no-type-check to skip); soli check is the standalone, whole-project form that never runs your app.