Current section
Files
Jump to
Current section
Files
lib/rustq/reach/smells/raw_rust_escape.ex
defmodule RustQ.Reach.Smells.RawRustEscape do
@moduledoc """
Detects large raw Rust escape hatches in RustQ generator code.
Tiny local escapes remain acceptable for macro invocations and parser gaps, but
multiline or long raw snippets should usually become Rusty-Elixir, RustQ AST,
source metadata, or a focused `defrustmacro`.
"""
use Reach.Smell.Check.AST
alias Reach.Smell.Finding
@kind :rustq_large_raw_rust_escape
@max_inline_chars 100
@impl true
def kinds, do: [@kind]
defp scan_ast(ast, file) do
{_ast, findings} =
Macro.prewalk(ast, [], fn node, findings ->
{node, raw_escape_findings(node, file) ++ findings}
end)
Enum.reverse(findings)
end
defp raw_escape_findings(node, file) do
with {:ok, meta, source} <- raw_escape(node),
true <- large_escape?(source) do
[
Finding.new(
kind: @kind,
message:
"large raw Rust escape; prefer defrust, RustQ AST/builders, metadata, or a small defrustmacro",
location: location(file, meta),
evidence: String.slice(source, 0, 120)
)
]
else
_not_raw_or_small -> []
end
end
defp raw_escape({name, meta, [source]})
when name in [:raw_expr!, :raw_pat!, :raw_stmt!, :raw_arm!] do
with {:ok, source} <- string_literal(source), do: {:ok, meta, source}
end
defp raw_escape({{:., meta, [_receiver, function]}, _call_meta, [source]})
when function in [:raw, :item, :impl_item, :stmt, :expr, :arm, :macro_item] do
with {:ok, source} <- string_literal(source), do: {:ok, meta, source}
end
defp raw_escape(_node), do: :error
defp string_literal(source) when is_binary(source), do: {:ok, source}
defp string_literal({:__block__, _meta, [source]}) when is_binary(source), do: {:ok, source}
defp string_literal(_source), do: :error
defp large_escape?(source) do
String.contains?(source, "\n") or String.length(source) > @max_inline_chars
end
defp location(file, meta), do: "#{file}:#{meta[:line] || 0}"
end