Download the PHP package doloto/big0nia without Composer
On this page you can find all versions of the php package doloto/big0nia. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download doloto/big0nia
More information about doloto/big0nia
Files in doloto/big0nia
Package big0nia
Short Description A standalone static analysis tool for PHP and Symfony that automatically detects algorithmic-complexity (Big-O) issues, performance anti-patterns, and nested-loop bottlenecks.
License MIT
Informations about the package big0nia
big0nia
Catches algorithmic-complexity and performance anti-patterns that PHPStan and Psalm don't check for.
A standalone static analysis tool for PHP and Symfony that automatically
detects algorithmic-complexity (Big-O) issues and performance
anti-patterns before they hit production: nested foreach/for loops
that scale at O(n²) or O(n × m) because they're secretly an expensive
collection join (including across a method/function call boundary),
in_array()/array_search() calls inside a loop that turn a lookup into
a linear scan, array_merge() calls that silently turn a loop into O(n²)
by rebuilding the same array on every iteration, and sorts that
redundantly re-sort data nothing in the loop ever changes.
big0nia is meant to run alongside PHPStan/Psalm, not replace them: they focus on type safety, bug patterns, and code smells, not algorithmic complexity.
Quick Start
That's it — no configuration file is required. See CLI reference for exit codes and options, or Configuration to exclude paths from analysis.
The problem it finds
This is O(users × orders): for every user, the whole $orders collection is
scanned. big0nia reports:
array_merge() in a loop
array_merge() copies its entire first argument on every call, so this is
O(n²) — for every item, the whole (growing) $result array is rebuilt.
big0nia reports:
Only the self-referential accumulation pattern is flagged — the assigned
variable must also appear as one of array_merge()'s own arguments, inside
a foreach or canonical for loop (through if/elseif/else, but not
into a nested loop, which is checked independently on its own). An
array_merge() call that builds an unrelated value from other variables is
not flagged, and the finding is suppressed under the same size-based rules
as the nested-loop-join detector when the loop provably iterates a small
fixed collection.
Repeated sorts in a loop
If $data is never modified inside the loop, sorting it again on every
iteration is pure waste — it's already sorted after the first call.
big0nia reports:
Only usort(), uasort(), and uksort() (the custom-comparator sorts)
are checked, inside a foreach or canonical for loop (through
if/elseif/else, but not into a nested loop, which is checked
independently on its own). If the sorted variable is reassigned anywhere
in the loop — including inside a nested loop — the sort is legitimate and
not flagged, and the finding is suppressed under the same size-based rules
as the other two detectors when the loop provably iterates a small fixed
collection.
Linear scans inside a loop
in_array() and array_search() scan their entire haystack argument every
time they're called, so calling one on every iteration of a loop is the
same O(n × m) blowup as a nested-loop join — it just doesn't look like one,
since there's no visible inner loop. big0nia reports:
isset(), array_key_exists(), and direct array-key access are never
flagged — those are already O(1) hash lookups, which is exactly the fix
this rule suggests. The finding is suppressed under the same size-based
rules as the other detectors when the scanned collection provably has a
fixed size, and when the loop itself is structurally bounded to a single
pass by an unconditional break/return/throw.
How nested-loop-join detection works
For every foreach loop, and every canonical indexed for loop
(for ($i = 0; $i < count($users); $i++) { ... $users[$i] ... } — exactly
this form; anything else, like <=, a precomputed bound variable, or
decrementing, is not recognized), big0nia:
- Finds a directly nested loop of the same kind — a
foreachinside anotherforeach's body, or a canonicalforinside another canonicalfor's body, seen through simpleifguards but not through other loops, closures, orelseif/elsebranches. Mixed nesting (aforinside aforeach, or vice versa) is not detected. - Finds a join comparison — an equality comparison (
===,==,!==,!=) between something rooted in the outer loop's item (theforeachvalue variable, or$collection[$index]for aforloop) and the inner loop's item, either as the wholeifcondition or combined with&&at any depth, e.g.$user->getId() === $order->getUserId() && $order->isPaid(). - Classifies the size of both compared collections — an array
literal used directly, a variable last assigned an array literal
earlier in the same statement list, or
$this->property/$this->method()resolved to a declared or constructor-promoted default array literal, or to a method body that is exactly onereturn <array literal>;. A property's default is only trusted if the property is never reassigned anywhere else in the class. - Suppresses only when provably fixed-size — a finding is
suppressed only when one of the two collections classifies as
fixed-size: an array literal of any size (the property that matters
is independence from input size, not smallness), a
self::/static::class constant array, or a literal-boundrange()call. Everything else, including a collection it can't classify at all, is reported.
Interprocedural nested-loop joins
When the inner loop lives not in the outer loop's own body, but inside a
method called from there — possibly in a different class or file — big0nia
traces the call chain and reports the join:
big0nia reports:
The detector resolves six call patterns: typed properties
($this->service->method()), local variables assigned via new
($x = new Foo(); $x->method()), static calls (Foo::method()), free
functions (helper()), and same-class calls via $this->method() or
self::method(). static::/late static binding and parent:: are not
resolved, since either could name a class this project doesn't have enough
information to identify safely. For interface-typed properties, the project
must have exactly one class that directly implements the interface. A local
new-assigned variable's type is trusted only when nothing reassigns it
afterward that this analysis can't fully see — a reassignment inside a
conditional branch, for instance — otherwise the finding is silently dropped
rather than guessed at. Only positional call arguments are matched — named
arguments or spread/unpacked arguments break the chain entirely. The call chain
can span multiple hops (up to a bounded depth), and the reported message names
the full chain; the tip always names the file and line of the actual inner
loop. Scope is foreach loops and canonical indexed for loops only — the
same narrow set the intra-procedural detector already documents — and while
loops are not yet supported for cross-call detection. Suppressions apply: a
finding is reported only when both the outer and inner collections are not
provably fixed-size (see "Suppresses only when provably fixed-size" above).
CLI reference
- Accepts any mix of files and directories; directories are scanned
recursively for
.phpfiles. - Exit code
0: every given path was analysed and no issues were found. - Exit code
1: a finding was reported, a given path doesn't exist, a file couldn't be parsed or read, or the config file is invalid. - A path that doesn't exist prints
Path not found: <path>to stderr and the rest of the run continues. - A file that fails to parse or can't be read prints
Skipping <path>: <message>to stderr and the rest of the run continues. - Memory usage scales with total analysed project size: every file is parsed
and its AST retained up front to build the cross-file symbol index. On very
large codebases, pass a higher memory limit to PHP if you hit an
out-of-memory error, e.g.
php -d memory_limit=1G vendor/bin/big0nia analyse src/.
Configuration
big0nia looks for a big0nia.neon file in the current working directory.
If none is found, every path passed on the command line is analysed in
full.
ignore_paths: a list of strings. Any file whose path contains one of these strings anywhere (a plain substring match, not a glob) is excluded from analysis entirely — it's never even parsed, so a syntax error inside an excluded path is silently skipped rather than reported.- A missing config file, or one with no
ignore_pathskey, excludes nothing. - A malformed config file, or an
ignore_pathsvalue that isn't a list of strings, is a fatal error: nothing is analysed, and the problem is reported on stderr.
Status
big0nia ships the nested-loop-join detector for foreach (NestedLoopJoinRule)
and canonical indexed for loops (NestedForLoopJoinRule), including
interprocedural detection when the inner loop lives across a method/function
call boundary (InterproceduralLoopJoinRule), the self-referential
array_merge()-in-a-loop detector (ArrayMergeInLoopRule), the
loop-invariant repeated-sort detector (RepeatedSortInLoopRule), and the
in_array()/array_search() linear-scan-in-a-loop detector
(LinearScanInLoopRule). Fixed-size collections (array literals of any
size, self::CONST/static::CONST array constants, and literal-bound
range() calls) and loops structurally bounded to a single pass by an
unconditional break/return/throw are recognized and excluded from
every rule above. A big0nia.neon config file supports one key so far,
ignore_paths (see Configuration above), to exclude vendor code or legacy
modules from analysis entirely. Cross-call detection for while loops and
more performance-anti-pattern rules (Doctrine N+1) are planned.
Contributing
Before opening a PR, run the same checks CI runs:
CI runs this matrix against PHP 8.2 and 8.3 on every push to main and
every pull request (see .github/workflows/ci.yml).
License
MIT