Releases: elixir-vibe/reach
Releases · elixir-vibe/reach
v2.5.0
Highlights
- Added conservative trivial delegate smell detection for pass-through
defdelegatecalls and hand-written same-argument forwarding functions. - Resolves aliases, Erlang module calls, and static generated delegate targets while avoiding documented facades, behaviour adapters, semantic aliases, and ambiguous local calls.
- Added conversion idiom smells for identity float arithmetic and one-shot
List.to_tuple/1+elem/2access. - Tightened Phoenix/Ecto dead-code checks to avoid reporting compile-time DSL macros and schema/migration declarations as unused results.
- Improved corpus scan robustness and Elixir frontend handling for unusual pipe-shaped macro heads.
v2.4.0
Highlights
- Added plugin-provided Phoenix, Ecto, and Oban smell checks for LiveView lifecycle mistakes, Ecto query pitfalls, SQL interpolation, money-like
:floatfields, and Oban argument shape issues. - Added project-local custom smell checks and plugin smell registration.
- Added security/source smells for unsafe atom creation, unsafe
binary_to_term/1, missing@external_resource, Ecto cross joins, and unpinned query values. - Added smell profiling and corpus scan scripts for performance and real-world validation.
- Improved smell performance with inferred source prefilters, shared source runners, selector consolidation, and faster semantic idiom checks.
- Tuned noisy checks against real-world Elixir projects.
Breaking-ish change
Source-level smell checks now use Reach.Smell.Check.Source instead of Reach.Smell.Check.Pattern. The unified smell/4 DSL also supports AST callback rules with mode: :ast.
Validation
Full CI passes locally.
v2.3.5
New
- Strict smell gates —
mix reach.check --smells --strictnow fails when non-baseline smell findings are present. Projects can also enable this withsmells: [strict: true]in.reach.exs. - Check baselines —
mix reach.checksupports--baseline PATH,--write-baseline PATH, andchecks: [baseline: PATH]to suppress known architecture/smell findings while failing on new ones.
Changed
- Combined checks —
mix reach.check --arch --smellsnow runs all requested check modes and shares one project analysis where possible. - Dogfooding — Reach now runs strict smell checking against itself in CI with tuned smell thresholds and an explicit baseline file.
Fixed
- Optional JavaScript support — compiling Reach without QuickBEAM no longer emits warnings about the unavailable JavaScript frontend.
- Self-smells — fixed actionable smell findings in Reach's own code and prevented pattern smell declarations from flagging themselves.
v2.3.4
v2.3.2
14 new smell patterns from Clippy, RuboCop, and SonarQube, all validated by hand against 200 top Hex packages.
New: case-on-boolean, case→match?, needless bool, manual max/min, @doc false on defp, sort+negative take, cond two-clause, unless/else, redundant assignment, redundant nil default, split→hd, filter→find, Map.new/MapSet.new conversions.
Fixed: ++ in reduce false positives (accumulator variable check), dogfooding across 15 files, CI runs --smells.
See CHANGELOG for details.
v2.3.1
New smell patterns
- Case-on-boolean —
case expr do true -> ...; false -> ... endwhen the subject is a boolean expression. Uses capture guards to avoid false positives on sentinel returns. - Cond two-clause —
cond do ... true -> ... endwith exactly two clauses →if/else - Unless/else →
ifwith positive case first - Redundant assignment —
result = expr; result→ justexpr - Manual max/min —
if a > b, do: a, else: b→Kernel.max/2 @doc falseondefp— redundant, private functions can't have docs- Sort then negative take —
Enum.sort |> Enum.take(-n)→Enum.sort(:desc) |> Enum.take(n)
Fixed
++in reduce false positives — now checks whether++actually references the accumulator variable. 17 fewer false positives across 200 Hex packages.- Dogfooding — fixed all actionable smell findings in Reach's own code across 15 files
- CI —
mix cinow runsreach.check --arch --smells
v2.3.0
New smell checks
- Repeated traversal — same variable traversed by 2+ different
Enumfunctions (e.g.Enum.max(list)+Enum.min(list)); suggestsEnum.reduce/3. - Nested enum —
Enum.member?/2nested insideEnum.map/filter/etcon the same variable (O(n²)); suggestsMapSet. - Multiple Enum.at — 3+
Enum.at/2calls on the same variable with literal indices; suggests pattern matching. - Append in recursion —
++ [item]in recursive tail calls (O(n²)); suggests prepend + reverse. - Piped Regex.replace —
text |> Regex.replace(~r/.../, "")where the pipe puts the string in the regex position; uses the new ExASTpiped()predicate to avoid false positives on direct calls.
New pattern smells
Map.keys/values→Enum.join/uniq/count/length/member?/sum/max/minList.foldr/3,Enum.min_by/max_by/dedup_bywith identity fnEnum.filter |> Enum.filter,Enum.map |> List.flatten/Enum.flat_mapEnum.sort/2 |> Enum.reverse,Enum.with_index |> Enum.reduceEnum.map_join(_, "", _)redundant separatorInteger.to_string |> String.graphemes,length(String.split) - 1,Enum.at(list, -1)
Other
- Parser warning suppression via
emit_warnings: false - ex_ast bumped to ~> 0.11.1
- Corpus-tested against 200 top Hex packages: 0 crashes, 0 false positives
v2.2.0
New
- Length comparisons — flags
length(list) == 0,0 == length(list), andlength(list) > 0; suggests list pattern matching,== [], or!= []. - Identity
Enum.uniq_by/2— flagsEnum.uniq_by(collection, fn x -> x end); suggestsEnum.uniq/1. - Identity
Enum.sort_by/2— flagsEnum.sort_by(collection, fn x -> x end); suggestsEnum.sort/1. length/1in guards — flags small literal comparisons in guards; suggests list pattern matching.
Fixed
- Regression coverage for bare literal
withclauses — keeps valid clauses such astrueinwithblocks from regressing. - CI — refactored the length-in-guard check to satisfy strict Credo nesting rules.
v2.1.0
New smell checks
- Enum.at/2 inside loop — flags O(n) indexed access inside loops (O(n²) total)
- List.delete_at/2 inside loop — same O(n²) concern
- Enum.count/1 without predicate — suggests
length/1(avoids protocol dispatch) - Map.put with variable key and boolean — suggests MapSet for membership tracking
- Map.values → Enum.all?/any?/find/filter/map — iterate the map directly
- Enum.map → Enum.max/min/sum — allocates intermediate list; use
*_byor reduce - List.foldl/3 — non-idiomatic; use
Enum.reduce/3 - String.graphemes → Enum.reverse → Enum.join — use
String.reverse/1 - Redundant negated guard —
when x != yafterwhen x == yon same variables - Destructure then reconstruct —
[a, b, c]pattern reassembled as[a, b, c]in body
Fixed
- Frontend crash on
import Mod, only: :macros— atom values are now handled correctly - Frontend crash on macro-generated AST — bare atoms in
withclauses, non-listelseclauses, and non-list handler clauses no longer crash - Enum.count/1 false positive — piped predicate form (
|> Enum.count(&pred)) no longer flagged - Dogfooded own smells — fixed 50 findings in Reach's own code
Improved
- Stress-tested on 4500+ Hex packages with zero crashes
v2.0.1
Fixed
- ex_ast dependency —
ex_astis now a regular dependency instead ofonly: [:dev, :test]. Pattern smell checks import ExAST at compile time, so the previous declaration made Reach uninstallable from Hex. (Fixes #14)
Improved
- Smell false positive reduction — narrowed loop antipatterns to accumulator loops and recursive operands, excluded compile-time constructs and formatting functions from redundant computation, removed debatable patterns (chained
String.replace,Enum.takewith negative count, sequentialEnum.filter). Validated on 19 Hex packages: 63% fewer findings, all remaining verified as true positives. - Module documentation — all public modules now have
@moduledocdescriptions.