Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Fixed

- Expand lint categories into supported type-aware rules before calling tsgolint, preserving explicit rule settings and per-file overrides.

## 0.18.1 - 2026-09-15

### Added
Expand Down
4 changes: 4 additions & 0 deletions guides/features/formatting-and-linting.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,7 @@ config :volt, :lint,
Volt keeps the Oxlint-style rule shape: configure normal and type-aware rules together under `:rules`. When `--type-aware` is enabled, Volt still runs the normal syntax lint path and also invokes `tsgolint` for supported semantic TypeScript rules.

Exits with non-zero status on issues.

With `--type-aware`, categories also select supported type-aware rules from enabled plugins. For example, `"correctness" => :deny` with `plugins: [:typescript]` enables `typescript/no-floating-promises`. OXC expands categories using its bundled rule registry before invoking `tsgolint`; category names are never sent to the executable. Individual rule settings override categories, and per-file overrides are applied before expansion.

Configurations without category entries keep their explicit type-aware rule selection. `--type-check` independently enables TypeScript compiler diagnostics.
15 changes: 8 additions & 7 deletions lib/volt/js/check.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ defmodule Volt.JS.Check do

lint_config
|> Volt.JS.Lint.Config.options(original)
|> Keyword.fetch!(:rules)
|> typescript_rules()
|> Keyword.take([:plugins, :rules])
|> Keyword.update!(:rules, &typescript_rules/1)
end)
|> Enum.sort_by(fn {rules, _files} -> rules end)
|> Enum.flat_map(fn {rules, batch} ->
case run_type_aware_lint(batch, Keyword.put(common_opts, :rules, rules)) do
|> Enum.sort_by(fn {options, _files} -> options end)
|> Enum.flat_map(fn {options, batch} ->
case run_type_aware_lint(batch, Keyword.merge(common_opts, options)) do
{:ok, diagnostics} ->
Enum.map(diagnostics, fn diagnostic ->
diagnostic |> restore_sfc_file(source_files) |> promote_type_check_diagnostic(opts)
Expand Down Expand Up @@ -159,7 +159,7 @@ defmodule Volt.JS.Check do
{:error, errors}

rule ->
rules = Map.delete(lint_opts[:rules], "typescript/#{rule}")
rules = Map.put(lint_opts[:rules], "typescript/#{rule}", :allow)
run_type_aware_lint(files, Keyword.put(lint_opts, :rules, rules))
end

Expand All @@ -179,7 +179,8 @@ defmodule Volt.JS.Check do

defp typescript_rules(rules) do
Map.filter(rules, fn {rule, _config} ->
String.starts_with?(to_string(rule), "typescript/")
to_string(rule) in ~w(all correctness suspicious pedantic perf style restriction nursery) or
String.starts_with?(to_string(rule), "typescript/")
end)
end

Expand Down
64 changes: 59 additions & 5 deletions test/mix/tasks/volt/js/check_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,54 @@
assert output =~ "typescript/no-floating-promises"
end

test "category-only configuration submits semantic rules to tsgolint" do

Check failure on line 81 in test/mix/tasks/volt/js/check_test.exs

View workflow job for this annotation

GitHub Actions / elixir / Elixir 1.20 / OTP 29

test category-only configuration submits semantic rules to tsgolint (Mix.Tasks.Volt.Js.CheckTest)

Check failure on line 81 in test/mix/tasks/volt/js/check_test.exs

View workflow job for this annotation

GitHub Actions / elixir / Elixir 1.18 / OTP 27

test category-only configuration submits semantic rules to tsgolint (Mix.Tasks.Volt.Js.CheckTest)
file = Path.join(@tmp_dir, "typed.ts")
File.write!(file, "Promise.resolve(1);\n")

Application.put_env(:volt, :lint,
tsgolint: fake_tsgolint!(@tmp_dir),
plugins: [:typescript],
rules: %{"correctness" => :deny}
)

diagnostics = Volt.JS.Check.lint([file], type_aware: true)

assert Enum.any?(
diagnostics,
&(&1.rule == "typescript/no-floating-promises" and &1.severity == :deny)
)
end

test "category expansion honors per-file semantic rule exclusions" do

Check failure on line 99 in test/mix/tasks/volt/js/check_test.exs

View workflow job for this annotation

GitHub Actions / elixir / Elixir 1.20 / OTP 29

test category expansion honors per-file semantic rule exclusions (Mix.Tasks.Volt.Js.CheckTest)

Check failure on line 99 in test/mix/tasks/volt/js/check_test.exs

View workflow job for this annotation

GitHub Actions / elixir / Elixir 1.18 / OTP 27

test category expansion honors per-file semantic rule exclusions (Mix.Tasks.Volt.Js.CheckTest)
first = Path.join(@tmp_dir, "first.ts")
second = Path.join(@tmp_dir, "second.ts")
Enum.each([first, second], &File.write!(&1, "Promise.resolve(1);\n"))
payload_path = Path.join(@tmp_dir, "categories.jsonl")

tsgolint =
fake_executable!(@tmp_dir, "tsgolint-categories", """
input = IO.binread(:stdio, :eof)
File.write!(#{inspect(payload_path)}, [input, "\\n"], [:append])
""")

Application.put_env(:volt, :lint,
root: @tmp_dir,
tsgolint: tsgolint,
plugins: [:typescript],
rules: %{"correctness" => :deny},
overrides: [%{files: ["second.ts"], rules: %{"typescript/no-floating-promises" => :allow}}]
)

assert [] = Volt.JS.Check.lint([first, second], type_aware: true)
batches = payload_path |> File.stream!() |> Enum.map(&Jason.decode!/1)
configs = Enum.flat_map(batches, & &1["configs"])

for {file, selected} <- [{first, true}, {second, false}] do
config = Enum.find(configs, &(Path.expand(file) in &1["file_paths"]))
assert Enum.any?(config["rules"], &(&1["name"] == "no-floating-promises")) == selected
end
end

test "type-check diagnostics are promoted to errors" do
diagnostic = %{
rule: "typescript/TS2322",
Expand All @@ -90,7 +138,7 @@
Volt.JS.Check.promote_type_check_diagnostic(diagnostic, type_check: true)
end

test "type-aware check keeps Oxlint-style rules and retries without unsupported tsgolint rules" do

Check failure on line 141 in test/mix/tasks/volt/js/check_test.exs

View workflow job for this annotation

GitHub Actions / elixir / Elixir 1.20 / OTP 29

test type-aware check keeps Oxlint-style rules and retries without unsupported tsgolint rules (Mix.Tasks.Volt.Js.CheckTest)

Check failure on line 141 in test/mix/tasks/volt/js/check_test.exs

View workflow job for this annotation

GitHub Actions / elixir / Elixir 1.18 / OTP 27

test type-aware check keeps Oxlint-style rules and retries without unsupported tsgolint rules (Mix.Tasks.Volt.Js.CheckTest)
File.write!(Path.join(@tmp_dir, "typed.ts"), "export const value = 1\n")
tsgolint = fake_tsgolint_unknown_retry!(@tmp_dir)

Expand All @@ -110,7 +158,13 @@
end)

payload = @tmp_dir |> Path.join("payload.json") |> File.read!() |> Jason.decode!()
assert [%{"rules" => [%{"name" => "no-floating-promises"}]}] = payload["configs"]
assert [%{"rules" => rules}] = payload["configs"]
assert Enum.any?(rules, &(&1["name"] == "no-floating-promises"))

refute Enum.any?(
rules,
&(&1["name"] in ["correctness", "suspicious", "consistent-type-imports"])
)
end

test "type-aware check submits framework single-file component scripts as virtual files" do
Expand Down Expand Up @@ -154,7 +208,7 @@
"svelteValue"
end

test "type-aware overrides batch by effective rules and use original SFC paths" do

Check failure on line 211 in test/mix/tasks/volt/js/check_test.exs

View workflow job for this annotation

GitHub Actions / elixir / Elixir 1.20 / OTP 29

test type-aware overrides batch by effective rules and use original SFC paths (Mix.Tasks.Volt.Js.CheckTest)

Check failure on line 211 in test/mix/tasks/volt/js/check_test.exs

View workflow job for this annotation

GitHub Actions / elixir / Elixir 1.18 / OTP 27

test type-aware overrides batch by effective rules and use original SFC paths (Mix.Tasks.Volt.Js.CheckTest)
files = Enum.map(["app.ts", "Component.vue", "Widget.svelte"], &Path.join(@tmp_dir, &1))
[app, vue, svelte] = files
File.write!(app, "export const value = 1;\n")
Expand All @@ -176,7 +230,7 @@
Application.put_env(:volt, :lint,
root: @tmp_dir,
tsgolint: tsgolint,
rules: %{"typescript/no-floating-promises" => :deny},
rules: %{"correctness" => :deny, "typescript/no-floating-promises" => :deny},
overrides: [
%{files: ["**/*.{vue,svelte}"], rules: %{"typescript/no-floating-promises" => :warn}},
%{files: ["**/*.script0.ts"], rules: %{"typescript/no-floating-promises" => :allow}}
Expand All @@ -196,9 +250,9 @@
configs = Enum.flat_map(batches, & &1["configs"])
assert Enum.sort(Enum.map(configs, &length(&1["file_paths"]))) == [1, 2]

assert configs |> Enum.flat_map(& &1["rules"]) |> Enum.map(& &1["name"]) |> Enum.uniq() == [
"no-floating-promises"
]
names = configs |> Enum.flat_map(& &1["rules"]) |> Enum.map(& &1["name"])
assert "no-floating-promises" in names
refute "correctness" in names

assert Enum.find(diagnostics, &(&1.file == Path.expand(app))).severity == :deny
assert Enum.find(diagnostics, &(&1.file == vue)).severity == :warn
Expand Down
Loading