|
| 1 | +defmodule OXC.Lint do |
| 2 | + @moduledoc """ |
| 3 | + Lint JavaScript/TypeScript source with oxlint's built-in rules |
| 4 | + and optional custom Elixir rules. |
| 5 | +
|
| 6 | + Combines native Rust performance for 650+ standard rules with |
| 7 | + the ability to write project-specific rules in Elixir using |
| 8 | + the same AST that `OXC.parse/2` returns. |
| 9 | +
|
| 10 | + ## Examples |
| 11 | +
|
| 12 | + {:ok, diags} = OXC.Lint.run("debugger;", "test.js", |
| 13 | + rules: %{"no-debugger" => :deny}) |
| 14 | +
|
| 15 | + {:ok, []} = OXC.Lint.run("export const x = 1;\\n", "test.ts") |
| 16 | + """ |
| 17 | + |
| 18 | + @type severity :: :allow | :warn | :deny |
| 19 | + @type diagnostic :: %{ |
| 20 | + rule: String.t(), |
| 21 | + message: String.t(), |
| 22 | + severity: severity(), |
| 23 | + span: {non_neg_integer(), non_neg_integer()}, |
| 24 | + labels: [{non_neg_integer(), non_neg_integer()}], |
| 25 | + help: String.t() | nil |
| 26 | + } |
| 27 | + |
| 28 | + @doc """ |
| 29 | + Lint source code with oxlint's built-in rules and optional custom rules. |
| 30 | +
|
| 31 | + ## Options |
| 32 | +
|
| 33 | + * `:rules` — map of rule names to severity (`:deny`, `:warn`, `:allow`). |
| 34 | + Rule names follow oxlint conventions: `"eqeqeq"`, `"react/no-danger"`, |
| 35 | + `"typescript/no-explicit-any"`, etc. |
| 36 | +
|
| 37 | + * `:plugins` — list of built-in plugin atoms to enable. |
| 38 | + Default: oxlint defaults (eslint correctness rules). |
| 39 | + Available: `:react`, `:typescript`, `:unicorn`, `:import`, `:jsdoc`, |
| 40 | + `:jest`, `:vitest`, `:jsx_a11y`, `:nextjs`, `:react_perf`, `:promise`, |
| 41 | + `:node`, `:vue`, `:oxc` |
| 42 | +
|
| 43 | + * `:fix` — compute fix suggestions. Default: `false` |
| 44 | +
|
| 45 | + * `:custom_rules` — list of `{module, severity}` tuples for Elixir rules. |
| 46 | + Each module must implement the `OXC.Lint.Rule` behaviour. |
| 47 | +
|
| 48 | + * `:settings` — arbitrary map passed to custom rule context. |
| 49 | +
|
| 50 | + ## Examples |
| 51 | +
|
| 52 | + # Built-in rules only |
| 53 | + {:ok, diags} = OXC.Lint.run("debugger;", "test.js", |
| 54 | + rules: %{"no-debugger" => :deny}) |
| 55 | +
|
| 56 | + # With specific plugins and rules |
| 57 | + {:ok, diags} = OXC.Lint.run(source, "app.tsx", |
| 58 | + plugins: [:react, :typescript], |
| 59 | + rules: %{"no-console" => :warn, "react/no-danger" => :deny} |
| 60 | + ) |
| 61 | +
|
| 62 | + # With custom Elixir rules |
| 63 | + {:ok, diags} = OXC.Lint.run(source, "app.ts", |
| 64 | + custom_rules: [{MyApp.NoConsoleLog, :warn}] |
| 65 | + ) |
| 66 | + """ |
| 67 | + @spec run(String.t(), String.t(), keyword()) :: {:ok, [diagnostic()]} | {:error, [String.t()]} |
| 68 | + def run(source, filename, opts \\ []) do |
| 69 | + plugins = opts |> Keyword.get(:plugins, []) |> Enum.map(&to_string/1) |
| 70 | + fix = Keyword.get(opts, :fix, false) |
| 71 | + |
| 72 | + rules = |
| 73 | + opts |
| 74 | + |> Keyword.get(:rules, %{}) |
| 75 | + |> Enum.map(fn {name, severity} -> {to_string(name), severity_to_string(severity)} end) |
| 76 | + |
| 77 | + custom_rules = Keyword.get(opts, :custom_rules, []) |
| 78 | + settings = Keyword.get(opts, :settings, %{}) |
| 79 | + |
| 80 | + case OXC.Lint.Native.lint(source, filename, plugins, rules, fix) do |
| 81 | + {:ok, builtin_diags} -> |
| 82 | + custom = |
| 83 | + case custom_rules do |
| 84 | + [] -> [] |
| 85 | + rules -> run_custom_rules(rules, source, filename, settings) |
| 86 | + end |
| 87 | + |
| 88 | + {:ok, builtin_diags ++ custom} |
| 89 | + |
| 90 | + {:error, errors} -> |
| 91 | + {:error, errors} |
| 92 | + end |
| 93 | + end |
| 94 | + |
| 95 | + defp severity_to_string(:deny), do: "deny" |
| 96 | + defp severity_to_string(:warn), do: "warn" |
| 97 | + defp severity_to_string(:allow), do: "allow" |
| 98 | + |
| 99 | + defp run_custom_rules(rules, source, filename, settings) do |
| 100 | + case OXC.parse(source, filename) do |
| 101 | + {:ok, ast} -> |
| 102 | + context = %{source: source, filename: filename, settings: settings} |
| 103 | + |
| 104 | + Enum.flat_map(rules, fn {module, severity} -> |
| 105 | + meta = module.meta() |
| 106 | + |
| 107 | + module.run(ast, context) |
| 108 | + |> Enum.map(fn diag -> |
| 109 | + %{ |
| 110 | + rule: meta.name, |
| 111 | + message: diag.message, |
| 112 | + severity: severity, |
| 113 | + span: Map.get(diag, :span, {0, 0}), |
| 114 | + labels: Map.get(diag, :labels, []), |
| 115 | + help: Map.get(diag, :help) |
| 116 | + } |
| 117 | + end) |
| 118 | + end) |
| 119 | + |
| 120 | + {:error, _} -> |
| 121 | + [] |
| 122 | + end |
| 123 | + end |
| 124 | +end |
0 commit comments