defmodule OXC.Lint do @moduledoc """ Lint JavaScript/TypeScript source with oxlint's built-in rules and optional custom Elixir rules. Combines native Rust performance for 650+ standard rules with the ability to write project-specific rules in Elixir using the same AST that `OXC.parse/2` returns. ## Examples {:ok, diags} = OXC.Lint.run("debugger;", "test.js", rules: %{"no-debugger" => :deny}) {:ok, []} = OXC.Lint.run("export const x = 1;\\n", "test.ts") """ @type severity :: :allow | :warn | :deny @type diagnostic :: %{ rule: String.t(), message: String.t(), severity: severity(), span: {non_neg_integer(), non_neg_integer()}, labels: [{non_neg_integer(), non_neg_integer()}], help: String.t() | nil } @doc """ Lint source code with oxlint's built-in rules and optional custom rules. Pass a list of files with `type_aware: true` to run TypeScript type-aware rules through `tsgolint` headless mode: OXC.Lint.run(["lib/app.ts"], type_aware: true, tsgolint: "tsgolint", rules: %{"typescript/no-floating-promises" => :deny}) ## Options * `:rules` — map of rule names to severity (`:deny`, `:warn`, `:allow`). Rule names follow oxlint conventions: `"eqeqeq"`, `"react/no-danger"`, `"typescript/no-explicit-any"`, etc. * `:plugins` — list of built-in plugin atoms to enable. Default: oxlint defaults (eslint correctness rules). Available: `:react`, `:typescript`, `:unicorn`, `:import`, `:jsdoc`, `:jest`, `:vitest`, `:jsx_a11y`, `:nextjs`, `:react_perf`, `:promise`, `:node`, `:vue`, `:oxc` * `:fix` — compute fix suggestions. Default: `false` * `:custom_rules` — list of `{module, severity}` tuples for Elixir rules. Each module must implement the `OXC.Lint.Rule` behaviour. * `:settings` — arbitrary map passed to custom rule context. ## Examples # Built-in rules only {:ok, diags} = OXC.Lint.run("debugger;", "test.js", rules: %{"no-debugger" => :deny}) # With specific plugins and rules {:ok, diags} = OXC.Lint.run(source, "app.tsx", plugins: [:react, :typescript], rules: %{"no-console" => :warn, "react/no-danger" => :deny} ) # With custom Elixir rules {:ok, diags} = OXC.Lint.run(source, "app.ts", custom_rules: [{MyApp.NoConsoleLog, :warn}] ) """ @spec run([String.t()], keyword()) :: {:ok, [diagnostic()]} | {:error, [String.t()]} @spec run(iodata(), String.t(), keyword()) :: {:ok, [diagnostic()]} | {:error, [String.t()]} def run(files, opts) when is_list(files) and is_list(opts) do if Keyword.get(opts, :type_aware, false) do OXC.Lint.TypeAware.run(files, opts) else {:error, ["OXC.Lint.run/2 with a file list requires type_aware: true"]} end end def run(source, filename, opts \\ []) do source = IO.iodata_to_binary(source) plugins = opts |> Keyword.get(:plugins, []) |> Enum.map(&to_string/1) fix = Keyword.get(opts, :fix, false) rules = opts |> Keyword.get(:rules, %{}) |> Enum.map(fn {name, severity} -> {to_string(name), severity_to_string(severity)} end) custom_rules = Keyword.get(opts, :custom_rules, []) settings = Keyword.get(opts, :settings, %{}) case OXC.Lint.Native.lint(source, filename, plugins, rules, fix) do {:ok, builtin_diags} -> custom = case custom_rules do [] -> [] rules -> run_custom_rules(rules, source, filename, settings) end {:ok, builtin_diags ++ custom} {:error, errors} -> {:error, errors} end end @doc """ Like `run/3` but raises on errors. """ @spec run!(iodata(), String.t(), keyword()) :: [diagnostic()] def run!(source, filename, opts \\ []) do case run(source, filename, opts) do {:ok, diags} -> diags {:error, errors} -> raise OXC.Error, message: "OXC lint error: #{inspect(errors)}", errors: errors end end defp severity_to_string(:deny), do: "deny" defp severity_to_string(:warn), do: "warn" defp severity_to_string(:allow), do: "allow" defp run_custom_rules(rules, source, filename, settings) do case OXC.parse(source, filename) do {:ok, ast} -> context = %{source: source, filename: filename, settings: settings} Enum.flat_map(rules, fn {module, severity} -> meta = module.meta() module.run(ast, context) |> Enum.map(fn diag -> %{ rule: meta.name, message: diag.message, severity: severity, span: Map.get(diag, :span, {0, 0}), labels: Map.get(diag, :labels, []), help: Map.get(diag, :help) } end) end) {:error, _} -> [] end end end