From 4ac88685ee5f7cbd69b224addaeb19bf92d253ab Mon Sep 17 00:00:00 2001 From: Roman Kurakin Date: Tue, 15 Sep 2026 21:39:27 +0500 Subject: [PATCH] fix: apply lint categories to type-aware checks --- CHANGELOG.md | 4 ++ guides/features/formatting-and-linting.md | 4 ++ lib/volt/js/check.ex | 15 +++--- test/mix/tasks/volt/js/check_test.exs | 64 +++++++++++++++++++++-- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 053df36..bac182a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/guides/features/formatting-and-linting.md b/guides/features/formatting-and-linting.md index 453caaf..ceacad1 100644 --- a/guides/features/formatting-and-linting.md +++ b/guides/features/formatting-and-linting.md @@ -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. diff --git a/lib/volt/js/check.ex b/lib/volt/js/check.ex index 696ee8f..1f639ff 100644 --- a/lib/volt/js/check.ex +++ b/lib/volt/js/check.ex @@ -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) @@ -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 @@ -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 diff --git a/test/mix/tasks/volt/js/check_test.exs b/test/mix/tasks/volt/js/check_test.exs index 85a0b60..4b207a7 100644 --- a/test/mix/tasks/volt/js/check_test.exs +++ b/test/mix/tasks/volt/js/check_test.exs @@ -78,6 +78,54 @@ defmodule Mix.Tasks.Volt.Js.CheckTest do assert output =~ "typescript/no-floating-promises" end + test "category-only configuration submits semantic rules to tsgolint" do + 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 + 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", @@ -110,7 +158,13 @@ defmodule Mix.Tasks.Volt.Js.CheckTest do 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 @@ -176,7 +230,7 @@ defmodule Mix.Tasks.Volt.Js.CheckTest do 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}} @@ -196,9 +250,9 @@ defmodule Mix.Tasks.Volt.Js.CheckTest do 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