ESC
Type to search...
S
Soli Docs

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, or Model.where("x = #{v}", { "v": v }).
  • Trusted.* → jailed File.* (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 enable for a specific rule re-enables only that rule, even if the prior disable was a blanket one.
  • A block disable with no matching enable runs 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 the soli binary (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.