Changelog
All notable changes to custos. Format: Keep a Changelog; versions follow Semantic Versioning.
[Unreleased]
[0.1.1] - 2026-10-08
Changed
- A suppression comment right before a call argument or an array item now applies to that argument or item, so one line of a multi-line call or array can be suppressed without silencing the whole statement.
Fixed
- Parser: in permissive mode, a property or promoted-parameter default followed by hooks is no longer read as a legacy
$a{0}offset. - JsonEncodingApiUsage fixes keep named arguments (appending
flags:orassociative:by name), extend a namedflags:value instead of giving up, parenthesise low-precedence flags ($p ? A : Bno longer becomes(JSON_THROW_ON_ERROR | $p) ? A : B), and write\JSON_THROW_ON_ERRORwhen the file qualifies its global constants. - MultiAssignmentUsage: consecutive offset reads from a string (
$a = $s[0]; $b = $s[1];) are no longer told to destructure, which would assignnullfrom a string. - NotOptimalIfConditions: filesystem calls (
is_file(),is_dir(),file_exists(), …) are no longer considered cheaper than in-memory checks; an operand is no longer moved ahead of a neighbour that narrows one of its variables (null/false comparison,instanceof,is_*()); property reads that run code (a PHP 8.4gethook, a virtual, abstract or interface property,__get()) cost like a method call and are never reordered; reads on an unknown receiver are left out of the comparison. - NotOptimalRegularExpressions: an escaped backslash before
.or$(\\\\.in a single-quoted pattern) no longer hides the metacharacter, so/sand/Dare not called pointless when they matter. - NullPointerException: a named argument is checked against the parameter of that name; a nullsafe check followed by an early exit, a
match (true)arm guarded by a nullsafe check, and a loop condition re-checked after acontinuenow narrow the variable; a property write no longer undoes a null check. - PhpUnitTests:
assertTrue(is_resource($h))/assertFalse(is_resource($h))are no longer rewritten toassertIsResource()/assertIsNotResource(), which treat a closed resource as a resource;empty()checks becomeassertEmpty()/assertNotEmpty()only when the operand is known to be a scalar, an array or null (PHPUnit countsCountableobjects), andassertTrue(!empty($x))names the assertion its fix writes. - PropertyInitializationFlaws: a property default equal to the default of the constructor parameter assigned to it is kept (objects built without the constructor, such as old serialized messages, still see it).
- ProperNullCoalescingOperatorUsage: a scalar fallback of another scalar type (
$id ?? 'new') and an array fallback for an iterable object ($node->attributes ?? [], a DoctrineCollection) are no longer reported. - SlowArrayOperationsInLoop: a
forcondition measuring a value the loop body visibly writes (element assignment,[] =,array_push(),unset(), reassignment) is no longer reported: its length changes on purpose. - StaticInvocationViaThis: with
EXCEPT_PHPUNIT_ASSERTIONS, an abstract restatement of a PHPUnit assertion (a trait'sabstract public static function assertIsResource(…)) called through$thisis exempt like PHPUnit's own assertions. - TraitsPropertiesConflicts: a trait property that re-declares a parent property only to attach attributes is no longer reported; a hooked property composed with a same-named trait property is an error, while the parent's hooks are ignored when a trait re-declares its property.
- UnnecessaryAssertion: a value reached through a nullsafe chain (
$r->find()?->sidebar()) is no longer said to be guaranteed by the last call's declared return type: the chain can yieldnull. - UnnecessaryCasting: a
(string)cast in a concatenation is reported only when its operand is known to be a string, int or float, not on nullable,bool,mixedor__toString()operands. - VariableFunctionsUsage:
call_user_func('name', …)is kept when a same-named namespace function or ause functionshadows the global function at the call (a wrapper reaching the builtin).
[0.1.0] - 2026-10-08
First public release.
Added
- 178 PHP inspections in 12 groups (probable bugs, performance, security, control flow, code style, unused code, PHPUnit, language-level migration 5.3 → 8.5, …), 108 of them with quick-fixes. Rules follow the target PHP version. Rule IDs are compatible with Php Inspections (EA Extended), so existing
@noinspectioncomments keep working;@custos-ignoreworks too. See the rule reference. - CLI:
analyse(text, json, checkstyle, github and sarif output;--fail-on; baselines for legacy code),fix(--dry-run,--diff, parallel),rules,explain,lsp,version. - Language server: diagnostics, quick-fixes, fix-all, "suppress for this statement" code action, and a background project index kept current through file watching. Setup guides for Neovim, Helix, VS Code, PhpStorm, Sublime Text and Emacs.
- Configuration through an optional
custos.json(paths, rules and their options, baseline); the PHP target falls back tocomposer.json. - Own PHP 5.3–8.5 parser (lossless, error tolerant) and semantic layer: a symbol index with embedded JetBrains phpstorm-stubs, type inference with narrowing, and PHPDoc support (generics and
@template, type aliases, array shapes, conditional return types,@param-out, phpstan/psalm assertions). - Distribution: release archives for Linux, macOS and Windows (amd64, arm64), a Homebrew cask (
brew install janalis/tap/custos) and a Composer package (composer require --dev janalis/custos) whose launcher downloads the checksum-verified binary. - Documentation site: https://janalis.github.io/custos/ (user guide, one page per rule with examples and quick-fix results, contributor guide).
Security
- Hostile input cannot crash, hang or exhaust the analyser: nesting depth, file size (10 MB), syntax errors and findings per file are capped, PHPDoc types are parsed in linear time, and known quadratic paths were removed.
- Only regular files are read, with size caps;
pathsandbaselinemust stay inside the project;custos fixnever writes through symlinks; the language server rejects malformed or oversized messages.
Notes
- custos is a clean-room implementation and intentionally differs from upstream where upstream behaviour is wrong (false positives, fixes that change semantics or produce invalid PHP).
