awesome.cr
awesome
Is your Crystal code awesome — or awful?
awesome is a next-generation semantic linter for Crystal. Where ameba checks style, awesome checks semantics and real best practices — the kind of things that make code faster or slower, not just prettier.
- Guards, not cops. Every check is a guard: semantic, documented, configured in
awesome.yml(rubocop.yml-style). - The compiler's own AST. awesome parses with
Crystal::Parser— the exact ASTcrystal buildsees. No reimplementation, no drift. - Documentation lives in the guard.
awesome docprints each guard's short description, good example and bad example — straight from the source of truth. - Zero dependencies. Just Crystal.
Install
With carat:
$ carat install OrelSokolov/awesome.cr
$ awesome
Or from a checkout:
$ git clone https://github.com/OrelSokolov/awesome.cr && cd awesome.cr
$ crystal build --release bin/awesome.cr -o bin/awesome
Usage
$ awesome # check the current directory (all guards)
$ awesome speed # speed guards only
$ awesome memory src lib/ # memory guards on specific paths
$ awesome code src/ # code guards only
$ awesome --level psycho # micro-optimization guards too (readability be damned)
$ awesome src foo.cr # check specific paths
$ awesome doc # documentation for all guards
$ awesome doc StructurePacking
$ awesome guards # registered guards: total, by level and kind
Output:
src/demo.cr:1:1
> struct Frame
[StructurePacking] Reorder struct fields by decreasing alignment (largest first): sizeof can shrink from 56 to 48 bytes
1 inspected, 1 issue.
Your code is awful: 1 issue.
Exit status: 0 — awesome, 1 — awful, 2 — error. CI-ready.
Issues are colored by severity — how serious a finding is:
- bug (red) — the code is broken at runtime: leaked or double-freed C memory, undefined behaviour.
- warning (yellow) — a real problem worth fixing: wasted allocations, O(n) traps, eager work.
- style (magenta) — design/idiom nits (smells): the code works, it just reads worse.
Check levels
Every guard has a level — how much it asks of your code. The level picks the audience: pick by how long the code is going to live.
| Level | Who it's for | What it holds | Default |
|---|---|---|---|
vibecoder |
Features first — startups validating a hypothesis, code that may be thrown away entirely next week. The bar: don't break on first run. | Runtime-breakers: leaked or double-freed C memory in lib shims. | always on |
senior |
Long-life code — it will be read, changed and supported for years, so maintainability pays off. | Best practices whose fix does not hurt readability: idioms like compact_map, streaming File.each_line, design smells. |
on out of the box |
psycho |
Experiments only — hot paths, audio callbacks, allocation budgets — or projects whose requirements make the normal rules unviable. | Micro-optimizations that trade readability for a small win: io << "a=" << a instead of puts "a=#{a}" saves one String allocation — true, but the line reads worse. |
opt-in |
Levels are cumulative: a run at level N executes every guard at level N or below. awesome checks up to senior by default. To go all the way (or strip down to the essentials):
$ awesome --level psycho
$ awesome --level vibecoder
Or set it once in awesome.yml (a per-guard enabled: true still turns a single psycho guard on without raising the global level):
level: psycho
Configuration
Drop an awesome.yml in your project root (rubocop.yml-style, guards instead of cops). Without one, awesome runs all guards with sane defaults:
include:
- src/**/*.cr
exclude:
- lib/**
guards:
StructurePacking:
enabled: true
check_extern: false
Guards
| Guard | Kind | Level | Checks | Docs |
|---|---|---|---|---|
| SelfSizeIndex | all | vibecoder | xs[xs.size] (or n = xs.size … xs[n]) — indexing by the collection's own size is always one past the end |
docs/guards/self_size_index.md |
| InclusiveRangeIndex | all | vibecoder | (0..xs.size).each { |i| xs[i] } — the inclusive bound yields i == xs.size on the last pass; use ... or times |
docs/guards/inclusive_range_index.md |
| IndexLookahead | all | vibecoder | xs.size.times { |i| xs[i + 1] } — lookahead indexing past the loop's last element, unless the body guards the boundary |
docs/guards/index_lookahead.md |
| StructurePacking | memory | senior | Struct fields ordered by decreasing alignment — no bytes wasted on padding | docs/guards/structure_packing.md |
| MemoryLeak | memory | vibecoder | C heap allocations from lib shims (Lib*.malloc) paired with a matching free in the project |
docs/guards/memory_leak.md |
| DoubleFree | memory | vibecoder | The same pointer never passed to a lib-shim deallocator twice without a reassignment in between | docs/guards/double_free.md |
| RaiseInLibCallback | memory | vibecoder | raise/reraise inside a proc handed to a lib-shim fun — the exception unwinds through C frames (undefined behaviour) |
docs/guards/raise_in_lib_callback.md |
| UncheckedLibPointer | memory | vibecoder | Pointers from T*-returning lib funs null-checked before .value — no LibFoo.find(x).value segfaults |
docs/guards/unchecked_lib_pointer.md |
| BoolBitmask | memory | senior | Structs with many Bool fields — a @[Flags] enum packs the same flags into one byte |
docs/guards/bool_bitmask.md |
| FileReadLines | memory | senior | File.read_lines slurps the whole file; File.each_line streams with flat memory use |
docs/guards/file_read_lines.md |
| InterpIntoIo | memory | psycho | Interpolation inside IO calls (io << "a#{b}c") — append the parts separately, zero intermediate Strings |
docs/guards/interp_into_io.md |
| LogEagerInterp | memory | senior | log.info "x=#{x}" interpolates even when muted; the block form is lazy |
docs/guards/log_eager_interp.md |
| ToSWithoutIo | memory | senior | def to_s without arguments — io <<, interpolation and puts all call to_s(io : IO) instead |
docs/guards/to_s_without_io.md |
| NilableStructField | memory | senior | Nilable fields in structs are tagged unions — Int64? is 16 bytes plus a branch, not 8 |
docs/guards/nilable_struct_field.md |
| AggregateAfterTransform | speed | senior | xs.map { … }.sum/.size/.empty? — the intermediate Array is pure garbage |
docs/guards/aggregate_after_transform.md |
| ArrayConcatInLoop | speed | senior | arr += [x] inside loops copies the whole Array every iteration; arr << x appends in place |
docs/guards/array_concat_in_loop.md |
| ArrayShiftInLoop | speed | senior | Array#shift/#unshift inside loops are O(n) per call — Deque is O(1) at both ends | docs/guards/array_shift_in_loop.md |
| MapCompact | speed | senior | xs.map { … }.compact — compact_map builds the result in one pass |
docs/guards/map_compact.md |
| MapFlatten | speed | senior | xs.map { … }.flatten — flat_map does one pass, no intermediate Array |
docs/guards/map_flatten.md |
| SelectCompareZero | speed | senior | xs.select { … }.size == 0 — none?/any? answer without the filtered Array |
docs/guards/select_compare_zero.md |
| SelectFirst | speed | senior | xs.select { … }.first — find stops at the first match, allocates nothing |
docs/guards/select_first.md |
| SelfSortReassign | speed | psycho | xs = xs.sort — the bang version does the same job in place (advisory) |
docs/guards/self_sort_reassign.md |
| SingleCharStringArg | speed | senior | Single-character String literals where a Char overload exists (split(",") → split(',')) |
docs/guards/single_char_string_arg.md |
| StringConcatInLoop | speed | senior | s += "…" inside loops — append into IO::Memory, materialize once after the loop |
docs/guards/string_concat_in_loop.md |
| TimesIndex | speed | senior | `xs.size.times { | i |
| UnusedIndex | speed | senior | each_with_index with an index nobody reads — plain each skips the bookkeeping |
docs/guards/unused_index.md |
| HardcodedDependency | code | senior | A class that declares an abstraction (@store : Store) but news up one of its ≥2 concrete implementations inside a method — the seam exists and the line welds it shut; without that evidence everything is silent |
docs/guards/hardcoded_dependency.md |
| GodClass | code | senior | A class carrying too many responsibilities — measured by method count (25) and instance-variable count (10) | docs/guards/god_class.md |
| DeepInheritance | code | senior | Inheritance chains three or more levels deep (hops between project classes; external parents cap the chain) | docs/guards/deep_inheritance.md |
| SingleSubclass | code | senior | A non-abstract base with exactly one subclass — speculative hierarchy | docs/guards/single_subclass.md |
| SubclassNoOverride | code | senior | A subclass overriding none of its parent's methods — inheritance for code reuse, not polymorphism | docs/guards/subclass_no_override.md |
| UtilityClass | code | senior | A class whose methods are all def self. with no instance behaviour — a namespace that should be a module |
docs/guards/utility_class.md |
| LongParameterList | code | senior | A method taking more parameters than a call site can read (default 7) — bundle them or split the method | docs/guards/long_parameter_list.md |
| NotImplementedOverride | code | senior | A subclass overriding a parent's concrete method with only raise NotImplementedError — the promise, revoked (LSP) |
docs/guards/not_implemented_override.md |
| BoolFlagParameter | code | senior | A Bool parameter fed both literal true and false at call sites — two behaviours behind one name |
docs/guards/bool_flag_parameter.md |
Development
$ crystal spec # run the test suite
$ crystal build bin/awesome.cr
examples/awful/ is a live demo: the bad snippets of every guard, one file per guard (a file may hold several examples of its guard). crystal spec asserts each guard fires on its own file — and nothing else fires there; run the binary to see the report:
$ bin/awesome examples/awful
A guard is a class in src/awesome/guards/:
class Awesome::Guards::MyGuard < Awesome::Guard
KIND = "speed" # or "memory", "code"
LEVEL = "senior" # or "vibecoder", "psycho" (senior is the default)
SEVERITY = "warning" # or "bug", "style" (warning by default; code kind → style)
SHORT_DESCRIPTION = "..."
GOOD_EXAMPLE = "..."
BAD_EXAMPLE = "..."
def test(source : Awesome::Source, config : Awesome::GuardConfig) : Array(Awesome::Issue)
visitor = MyVisitor.new(source)
source.ast.try &.accept(visitor)
visitor.issues
end
end
Awesome::Guard.registered << Awesome::Guards::MyGuard
See ROADMAP.md for what's next (search modes, more guards).
License
MIT — see LICENSE.
awesome.cr
- 0
- 0
- 0
- 0
- 0
- 27 minutes ago
- November 6, 2026
MIT License
Fri, 06 Nov 2026 15:33:40 GMT