JavaScript#
SafeLint analyses JavaScript source for the Holzmann "Power of Ten" safety rules, function length, nesting depth, cyclomatic complexity, error-handling discipline, hidden side effects, dataflow taint, and other classes of bug that style linters like ESLint don't catch. JavaScript support landed in v1.13.0 alongside Python; the analysis is runtime-agnostic and runs identically across Node.js, browser, Deno, Cloudflare Workers, Bun, and WASM-hosted JS engines.
File extensions#
.js, .mjs, .cjs. JSX (.jsx) is not yet registered, see the language roadmap. TypeScript (.ts / .tsx / .as) lives on its own language page as of v2.0.0.
Quick start#
pip install 'safelint[javascript]' # the tool itself runs on Python; the extra adds the JS grammar
safelint check src/ # lint a directory (git-modified files by default)
safelint check --all-files . # lint everything
safelint check --format json src/ # machine-readable for editors / CI
If your project doesn't already have a Python toolchain, the same install command works through pipx install 'safelint[javascript]' (isolates the install) or by adding 'safelint[javascript]' as a dev dependency through any pip-compatible manager. v2.0.0 ships every language grammar as an opt-in extra; the [javascript] extra installs tree-sitter-javascript alongside the engine.
Rules that fire on JavaScript#
18 user-facing rules apply to JavaScript: the 16 cross-language rules (Python / JS / TS / Java) plus SAFE302 global_mutation (Python / JS / TS only, not ported to Java yet) plus the 1 JavaScript-family rule (SAFE305 wide_scope_declaration). The 2 Python-only rules (SAFE201, SAFE301) and the 4 Java + Spring Boot only rules (SAFE901-904) are skipped automatically by the engine's per-language dispatch.
| Code | Rule | Notes for JavaScript |
|---|---|---|
| SAFE101 | function_length |
Default cap 60 lines. Counts function declarations, function expressions, arrow functions, generators, and class methods uniformly. count_mode = "statements" is Python-only, JS files use lines (default) or logical_lines. |
| SAFE102 | nesting_depth |
Counts if / for / for…in / while / do…while / switch / try blocks. Default max 2. Optional chaining (?.) does not count toward depth. |
| SAFE103 | max_arguments |
Counts named, default-value (b = 5), rest (...args), and destructured ({a, b} / [x, y]) parameters. Default cap 7. No self / cls skip. |
| SAFE104 | complexity |
Cyclomatic complexity, every if / else if / for / while / case / catch / ternary / && / || / ?? adds one. Default cap 10. |
| SAFE202 | empty_except |
Fires on catch (e) {}, catch {} (ES2019 optional binding), catch (e) { ; }, catch (e) { 0; }, catch (e) { "TODO"; }. |
| SAFE203 | logging_on_error |
Requires console.{log,info,warn,error,debug,trace} (or generic logger.* / pino.* / bunyan.*) in every catch. throw e; (exact rethrow of the caught binding) is exempt. |
| SAFE302 | global_mutation |
Function-body writes to a configured global namespace. Default: globalThis / window / global / self / process. Bracket-notation (window["x"] = 1) and update-expressions (process.exitCode++) are also covered. |
| SAFE303 | side_effects_hidden |
Functions named with a pure-prefix (get, calculate, is, …) that secretly call console.log / fetch / fs.readFile. Arrow functions bound via const fetchUser = () => ... are resolved via the enclosing variable_declarator. |
| SAFE304 | side_effects |
Any function calling an I/O primitive whose name doesn't signal I/O (no log / write / read / fetch / send / load substring, case-insensitive, so it matches fetchUser). |
| SAFE305 | wide_scope_declaration |
JavaScript-only. Fires on every var declaration, var is function-scoped (hoisted), let / const are block-scoped. |
| SAFE401 | resource_lifecycle |
Tracked acquirer calls (createReadStream, connect, Worker, …) must be inside a try { ... } finally { ... }. Constructor invocations (new Worker(...)) are also recognised. The newer using syntax (Stage 3 / Node 22+) isn't yet treated as a safe form. |
| SAFE501 | unbounded_loops |
while (true) with no break. Nested parens (while ((true))) are handled. The non-comparison-condition heuristic stays Python-only, JS idioms like while (queue.length) are commonly bounded. |
| SAFE601 | missing_assertions |
Functions with zero assertion calls. JS configures via assertion_calls_javascript (default: assert, ok, equal, expect, should, console.assert, …). Disabled by default. |
| SAFE701 | test_existence |
Every source file should have a matching <stem>.test.<ext> (Jest) or <stem>.spec.<ext> (Mocha) under test_dirs. Disabled by default. |
| SAFE702 | test_coupling |
Same pattern as SAFE701: if you change src/foo.js, you must also change foo.test.js in the same commit. Disabled by default. |
| SAFE801 | tainted_sink |
Function parameters / prompt() / getItem() flowing into eval / Function / child_process. new Function(userInput) is also recognised as a sink. Disabled by default. |
| SAFE802 | return_value_ignored |
Bare calls to fs.writeFile, stream.write, dispatchEvent, etc., the returned Promise / boolean / handle carries information that's being discarded. Disabled by default. |
| SAFE803 | null_dereference |
document.getElementById("id").textContent, DOM lookups / array find / collection get can return null or undefined. Optional chaining (?.) is recognised as a safe form. Disabled by default. |
The 2 rules not registered for JavaScript:
- SAFE201
bare_except, JavaScriptcatchclauses always bind the caught error and don't have theKeyboardInterrupt/SystemExithijack hazard, so there's no equivalent rule to fire. - SAFE301
global_state, JavaScript has noglobalread-only declaration form; on JS this would always be a strict subset of SAFE302 (global_mutation).
Runtime presets#
JavaScript source is the same regardless of where it runs, but the APIs it interacts with differ. The [tool.safelint.javascript] table selects which API surface the JavaScript rule defaults assume, sinks for taint analysis, tracked acquirers for resource-lifecycle, global namespaces for SAFE302, etc.
| Runtime | When to pick it | What changes |
|---|---|---|
node (default) |
Backend Node.js apps, CLIs, serverless functions running on Node-compatible runtimes | Node fs / child_process / process / streams. |
browser |
Browser-side JS, ES module bundles, anything running in a <script> or via a bundler targeting browsers |
Web APIs only. DOM lookups (getElementById, querySelector) for SAFE803; observers / Workers / WebSocket / ReadableStream for SAFE401; localStorage / addEventListener / postMessage for SAFE802; globalThis / window / self / document global namespaces for SAFE302. Drops Node fs and child_process entirely. |
deno |
Deno scripts and Deno Deploy applications | Deno.* API surface. Deno.open / Deno.connect / Deno.listen for SAFE401; Deno.run / Deno.Command for SAFE801; Deno added to global namespaces; process and window dropped. |
cloudflare-workers |
Cloudflare Workers (V8 isolates); also a reasonable starting point for other Web-API-only edge runtimes | KV / R2 / Durable Object methods (put / delete / get), Request body methods (json / formData / arrayBuffer / blob) as taint sources, minimal global-namespace list. No fs surface. |
bun |
Bun runtime | Node defaults plus Bun-specific extras (Bun.serve, Bun.spawn). |
WASM-hosted JS engines (QuickJS-WASM, Boa, V8 in WASM, etc.) execute the same JS source, pick whichever preset matches the API surface the engine exposes (typically browser if the host provides Web APIs, or a minimal subset if it only exposes ECMAScript built-ins). Source-language analysis itself is identical across all runtimes, only the defaults change.
See JavaScript runtime presets for the per-preset config details.
Configuration#
SafeLint reads its config from [tool.safelint] in pyproject.toml, or from a standalone safelint.toml at the project root. JavaScript projects that don't already have a pyproject.toml typically prefer the standalone form.
safelint.toml (typical JS-only project):
mode = "ci" # "local" (fail-on=error) or "ci" (fail-on=warning)
ignore = ["SAFE701"] # rules suppressed project-wide
[javascript]
runtime = "browser" # node / browser / deno / cloudflare-workers / bun
[per_file_ignores]
"*.test.js" = ["SAFE101", "SAFE601"] # tests routinely have longer functions
"vendor/**" = ["*"] # ignore everything under vendor/
[rules.function_length]
max_lines = 80 # raise the default cap
[rules.tainted_sink]
enabled = true # opt into the dataflow rules
sinks_javascript = ["eval", "Function", "myCustomDangerousFunction"] # overrides the browser preset
pyproject.toml (mixed Python + JS project):
# Same content but add the [tool.safelint.*] prefix
[tool.safelint]
mode = "ci"
ignore = ["SAFE701"]
[tool.safelint.javascript]
runtime = "browser"
[tool.safelint.per_file_ignores]
"*.test.js" = ["SAFE101", "SAFE601"]
[tool.safelint.rules.tainted_sink]
enabled = true
sinks_javascript = ["eval", "Function", "myCustomDangerousFunction"]
User-explicit _javascript-suffixed config keys always win over the runtime preset, the preset only changes the default list.
Installing the JavaScript extra#
JavaScript grammar support ships as an optional extra so Python-only projects don't pay for it:
pip install 'safelint[javascript]' # adds .js, .mjs, .cjs
# or, for TS projects that also have JS:
pip install 'safelint[typescript]' # bundles tree-sitter-javascript too
# or kitchen-sink:
pip install 'safelint[all]'
Without the extra, safelint check skips .js / .mjs / .cjs files with a one-line install hint at lint time. Heads-up for CI: if the run discovers JS files but every one is skipped because the grammar isn't installed, safelint exits with code 2 (the silent-failure guard) so a CI pipeline can't accidentally report green when no linting actually happened. If your CI logic distinguishes "lint clean" from "lint setup is broken", branch on exit code 2, or just install the matching extra and the guard never fires.
Pre-commit integration#
# .pre-commit-config.yaml
repos:
- repo: https://github.com/shelkesays/safelint
rev: v2.1.0rc1 # pin to a release (use a recent tag; v2.1.0rc1 also unlocks the Java extra if you later add .java files)
hooks:
- id: safelint
# JS / TS users add the matching extra so pre-commit's isolated
# environment installs the right tree-sitter grammar.
additional_dependencies: ['safelint[javascript]']
# The published hook's `types_or` already includes python,
# javascript, ts, and tsx. Optional: scope to a directory.
files: ^src/
JavaScript projects don't need a Python interpreter on developer machines, pre-commit itself manages the safelint runtime in an isolated environment. CI integration is the same: drop safelint check into your existing GitHub Actions / GitLab CI / etc.
JavaScript-specific config keys#
Each rule that runs on JS has a _javascript-suffixed config key parallel to its Python counterpart. Setting these explicitly overrides the active runtime preset.
[tool.safelint.rules.side_effects_hidden],io_functions_javascript(e.g.["log", "error", "fetch", "writeFile"])[tool.safelint.rules.side_effects],io_functions_javascript[tool.safelint.rules.global_mutation],global_namespaces_javascript(default depends on preset)[tool.safelint.rules.resource_lifecycle],tracked_functions_javascript[tool.safelint.rules.missing_assertions],assertion_calls_javascript[tool.safelint.rules.tainted_sink],sinks_javascript,sanitizers_javascript,sources_javascript[tool.safelint.rules.return_value_ignored],flagged_calls_javascript[tool.safelint.rules.null_dereference],nullable_methods_javascript
All of these accept a list of strings; bare-string typos like "log" (instead of ["log"]) raise a clear TypeError instead of silently coercing into a set of characters.
Contributing#
Want to refine a rule's JavaScript behaviour, add a runtime preset, or fix a parser edge case? See Adding a language for the architecture overview, or open an issue / PR against the main repo. TypeScript (including TSX and AssemblyScript) reuses this infrastructure end-to-end, see the TypeScript language page for the per-language notes.