It is my fervent wish that this file guide every AI coding agent working with code in this repository.
Any distilled, agent-facing documentation for this package - how it works
internally and the rationale behind key design decisions - lives in docs/.
Consult it before non-trivial changes; it is the source of truth from which the
public manual is distilled.
This is not a linter of its own - it is a thin orchestrator over two third-party
engines, and the expensive knowledge is in how they are wired (dual engines,
mirrored preset trees, override layering), not in the rule lists (grep the preset
files for those). Read docs/internals.md before non-trivial changes.
Nette Coding Standard is a unified CLI checker/fixer (ecs) that enforces the
Nette coding standard by driving PHP CS Fixer (automated fixing, .php presets)
and PHP_CodeSniffer (static analysis, .xml presets), with a handful of custom
fixers/sniffs and version-specific presets.
- PHP Version: 8.0 - 8.5
- Package:
nette/coding-standard
# Check (dry-run) / fix, with optional paths (default: src tests) and preset
./ecs check src tests
./ecs fix src tests --preset php81
# Extra config layered on top (.php -> Fixer, .xml -> Sniffer; repeat for both)
./ecs fix src --config-file ./overrides.php --config-file ./overrides.xml
# Sniff tests (Nette Tester)
vendor/bin/tester tests -s # or: composer tester
# Validate custom fixers/sniffs against the fixtures
./ecs check examples/ValidClass.php
./ecs check examples/InvalidConstructs.php # should report violationsInstalled globally via composer create-project nette/coding-standard /nette-cs,
then run /nette-cs/ecs ... from anywhere.
- The tool dogfoods its own standard (run
./ecs fixon it). Sniff tests aretests/SniffTestRunner.phptwithtests/fixtures/*.inc(+ optional.inc.expected; a leading<?php // {"prop": val}JSON comment injects sniff properties).
- Two independent engines run sequentially over one file list. A given rule is
owned by exactly one engine; there is no unified source of truth for "the Nette
style" - it's the union of both. Adding a version rule usually means editing both
preset trees (
preset-fixer/phpNN.phpandpreset-sniffer/phpNN.xml). - The two preset trees are mirrored by identical name, and a one-sided preset is a SILENT no-op - if a name resolves in only one tree, the other engine returns success without doing anything. Keep the trees in lockstep.
- Both engines are subprocesses, so everything crosses via a file or env var. The
file list reaches them asymmetrically: the sniffer via
--file-list=filelist.tmp, butpreset-fixer/base.phpreads../filelist.tmpitself.setPaths()must run first (it also applies the@phpVersionfilter). - The fixer
requirechain shares scope, and it's load-bearing.base.phpdiscoversncs.php/ the CLINCS_CONFIG_FILE_PHPoverride into$customRules, whichphp80.phpfolds into the merge - wrapping a preset in a function or renaming that variable silently drops all overrides. Merge operators differ on purpose:array_merge(right wins) inphp80.phpvs+(left wins) inphp81+. - The four
src/Fixer/*shadow their upstream fixers ("disable stock, enableNette/*"). Priority ordering matters (method_argument_space30, thenbraces_position-2, thenstatement_indentation-3).MethodArgumentSpaceFixeronly touches CALLS, not declarations;ClassAndTraitVisibilityRequiredFixerreflection-wraps afinalupstream fixer. - The sniffer wrapper ruleset (preset ->
ncs.xml-> CLI xml) is built only for aphpDD-named preset - a custom-named preset silently ignores project sniffer overrides.src/NetteCodingStandard/ruleset.xmlis vestigial; custom sniffs are pulled by file path inNette.xml(OptimizeGlobalCallsonly inoptimize-fn.xml). - Discovery is asymmetric:
ncs.phpis walked up from CWD,ncs.xmlis read only at the detected project root - they diverge when CWD != root. Exit codes:phpcbfreturning1means it fixed something (success). - The actual rule catalogs live in the preset files (
preset-fixer/common/*.php,preset-sniffer/Nette.xml, ...) - read those, don't mirror them here. User-facing usage (ncs.php/ncs.xmloverrides, CI setup) is in the README / web manual.
preset-sniffer/optimize-fn.xml (opt-in, sniffer-only - it deliberately has no
fixer-tree mirror) configures OptimizeGlobalCallsSniff to generate grouped
use function / use const imports and strip the corresponding \ prefixes
from the code body. What gets imported is intentionally narrow:
- Functions: with
optimizedFunctionsOnly=trueonly the calls the PHP compiler replaces with special opcodes - the list is hardcoded in the sniff ($compilerOptimizedFunctions, taken fromzend_compile.c):strlen, theis_*family,boolval/intval/floatval/strval,defined,chr,ord,call_user_func(_array),in_array,count/sizeof,get_class,get_called_class,gettype,func_num_args,func_get_args,array_slice,array_key_exists,sprintf. Importing these is what unlocks the opcodes; other global functions gain nothing, so they are not imported. - Constants: only names matching
includedConstantsin the preset -PHP_*andDIRECTORY_SEPARATOR.TRUE/FALSE/NULLare always ignored (hardcoded). - Existing
use function/use constimports outside these lists are kept as long as the symbol is still used in the file; imports are merged into one alphabetically sorted grouped statement per kind.