Repository navigation
hygiene.__invokeCall: required, calling an invokable object through __invoke() - #7
kaskader201 wants to merge 47 commits into
Conversation
421c1d0 to
487280f
Compare
A file of over a thousand lines is hard to read, so the workers got one of their own, as the commands coming next do. What the tests share went to Application.helpers.php.
…uration naming it A baseline the configuration names must exist, and the header of check names the one in use. Baseline finds, opens and names its file itself, the default name one constant of its own; RunnerFactory and CheckCommand only call it.
…abel() Its width is Markup::LabelWidth; the header of a run, check, fix and config use it.
With the functions of the container configurator of Symfony among those the packages bring.
PER Coding Style with what Pint 1.32 decides otherwise and without what it leaves to the author: the concatenation without spaces, `! $x`, `new Foo`, `fn ($x) =>`, the trailing comma in arrays alone, a blank line before `return` and `continue`, the enum cases kept together, the names in doc comments imported, and no construct spread for its width. A standard of the core beside the others, so the reference and explain list it with them.
The rules of PHP CS Fixer, PHP_CodeSniffer and Slevomat translated to the decisions of DressCode, their options included, a rule switched off making what it stands for keep, and what cannot be carried over said; the sets of PHP CS Fixer and of PHP_CodeSniffer translate to the standards, @symfony to symfony. A translation is written as a Config\Draft of the configuration, as NEON or PHP, each key with its comment, and a configuration file of PHP CS Fixer is run in a process of its own after the autoload of its project (Engine\ProcessRunner).
…PHP_CodeSniffer or Laravel Pint It writes it as dresscode.neon, or as the NEON or PHP file --output names, never over an existing one, and says what it could not carry over. A pint.json is read as data, its preset the standard of that name and its rules those of PHP CS Fixer.
…ering it The catalogue, explain and the reference list the names of the other tools each decision covers, and such a name written as a key, in only, fixRisky or severity is refused with the decisions that cover it, pointing to migrate config.
A name of another tool stands for the decisions covering it. phpcs:ignore, phpcs:disable, phpcs:enable, phpcs:ignoreFile and the annotation @phpcsSuppress silence what Interop translates the name of the rule to, a message of a sniff standing for its own row of the table.
…ct has Interop\FormatterSearch finds where a project keeps the configuration of its formatter and chooses it by the evidence that the project runs it, a script of composer.json, then a package it requires, then the file alone, a terminal asking among several; a file of PHP CS Fixer, which reading runs, is read only after the user agrees.
… as Runner::$files
…d holds Without git the scope is what the autoload and the usual directories hold. Measuring\Scope leaves out the data of tests and what the defaults exclude, and with git a file linguist-generated marks; every path it leaves out the run says is a change. The configuration is written into the root of the project alone, the paths it names being relative to it.
Measuring\Scope takes the paths of the formatter (<file> of PHP_CodeSniffer, ->in() of PHP CS Fixer, read from its syntax tree by Interop\PhpCsFixerSource) where its configuration names them, leaves out what it excludes and says what of its finder it could not read.
… of a sniff They keep the decisions of that sniff there.
For migrate suppressions to rewrite, counting every form the rewrite takes.
In place of the directory of the configuration, so that a configuration written elsewhere runs over the project with its composer.json. 21def661-e3da-4778-b400-5c949a404d2e
It counts by decision what disagrees with the configuration and what a fix would change. ProcessProbeRunner runs the probes as processes of this DressCode, a check of each configuration with --root over the project, concurrently and never a probe within a probe; InProcessProbeRunner runs them in the process. Measurement reads what the code says of a decision from the counts under each of its values (Classification).
dresscode init measures the code of a project and writes the dresscode.neon that fits it. The scope is what the formatter of the project checks where its configuration names the paths, else what git tracks, written as the entries of the root that hold it, else what the autoload of composer.json names; what it leaves out goes to excludePaths with the reason: a file a generator wrote, the data of tests, what git ignores inside the paths and a file that does not parse. The sample is shared among the directories. It chooses the standard under which a fix alone changes the fewest files of the sample, the one the formatter names, then perCs deciding a close call, laravel checked only for a project of Laravel or Laravel Pint. It measures the residue of that standard, every count a probe, a check of a configuration run as a process of this DressCode, concurrently: the indentation, the width of a line and the line ending first, then each decision under the values its domain tells. It writes what the code holds to and leaves unchecked what it does not decide, checks the configuration over the whole scope and leaves unchecked the decisions the others change least first until the first fix changes no more files than the budget, and names the baseline for what no rule fixes. --set writes a decision whatever the code says of it, --dry-run prints the configuration, --formatter names the configuration of the formatter and --format json writes the proposal as data; over a configuration that exists init writes nothing and says what has moved since or what the file sets apart from the standard the code holds to.
Where a fix under perCs changes at most half again as many files and the formatter does not name psr12, PSR-12 saying nothing of what PHP brought since.
…of their own The spinner turns through the one long probe of the check of the whole scope, and the progress bar of check turns it beside a slow file in place of counting its seconds, the wait for the workers lasting at most 0.1 s while the progress is drawn.
A close call of standards first, then in rounds the decisions over the budget, contradicted by the formatter or naming names that share an affix for naming.except, the answers numbered with the recommended one first, which Enter takes. The first round asks the five costliest, a later one what the answers left over the budget once the configuration is checked again; an answer that keeps the value of the code writes it as the user's, which the budget then leaves alone. Without a terminal, with --yes and with --format json it takes the recommended answers, which the JSON lists with how to give another. What the standard as it is would rewrite is said on the line Measured.
…eck, fix and baseline share
A tree with millions of them no longer runs out of memory. `fix` and `check` over 1.8 million violations held every one of them to the end of the run, about 1.5 GB in the parent, only to count them; the limit of 512 MB the program sets ends such a run sooner. A reporter still sees each file whole in `reportFile()`, and what a run keeps of it is `ViolationTally`. The baseline and the probes need the violations one by one, so they take them through `CollectingReporter`. b0961ab5-dcf4-44a9-a919-74d28c9f3060
…uires In the style the project keeps. dresscode upgrade today lays the set deprecations, with --modernize also modernizations, over the presets the configuration uses and beneath its own decisions and overrides, so that what the project keeps stays kept, lets in the plugins of the packages of upgrading data, adds the types of the code and fixes. Without a configuration file it takes the scope init would write and no style. With one it keeps the scope, the target, the types, fixRisky, the baseline and the maps, and of the style only the decisions the code keeps: a check without the types and the sets runs first, every decision it finds violated anywhere is left out, and the run is narrowed by only to the rest, the sets and the packages whose data reach the project, which a worker gets through --only. It takes --only, --fix-risky and --ask-risky. The upgrade goes to the lowest versions of PHP and of the libraries the project allows; the console says which libraries it takes, which packages are installed newer than that, whose declarations then say nothing of what it deprecates, advising composer update --prefer-lowest, and, where it rewrote a file, to run it again. The files a generator wrote are left out, and the console says how many.
…alled beside DressCode A package of upgrading data says which packages it is about under extra.dresscode.packages, and UpgradingTester::collectPackageProblems() refuses a list that is not exactly the packages its upgrading files are about. upgrade today learns of the packages from Packagist, asked once a day and remembered in the cache directory, for now of the vendor dresscode alone, and PackageDiscovery finds those installed beside DressCode by their type dresscode-plugin, not only those of the project. A project requiring one of their libraries itself gets a missing package installed first, through Upgrade\Composer: composer require into the installation of DressCode, or require --dev into the project where its lock holds DressCode, in a terminal after asking and without one only with --yes, otherwise it refuses with the command to run. The command then starts again in a new process, as the program of this package, whose autoloader knows the installed packages and the plugins they bring.
Layer names the library, the section and the version `from` says its fixes are right from, and a rule reporting an entry passes it on to Violation::$source, which the format json writes as `source`; ForbiddenSymbolsRule and ForbiddenMembersRule tell an entry of a package by it, and an entry of an attribute map names its origin as MemberMapEntry does. A package of upgrading data named in only stands for the requirement laying its data too.
A string naming a class the maps rename or forbid and a place reached by a computed name, which an upgrade cannot see. UnseenReferencesRule reports them under upgrading.unseen.* with the word `reported` and fixes nothing.
…nging nothing Upgrade\Estimator measures it by one check over the sets of the upgrade and the upgrading data, in workers, the targets of the command line, PHP and packages, laid over those of the configuration before the data are cut, so that the sections of versions the project has not moved to yet count as reached, and the names resolved for certain where the configuration says nothing of them. Every place is told by its topic, the library of its entry or its decision, by the bridge or the jump, and by what a run can do about it; the estimate goes topic by topic with what works today and what after the raise, as text or as a JSON document. It counts what an upgrade cannot see (BlindSpots): what UnseenReferencesRule reports, the files no rule reads by their extension and the generated files it leaves out. It names the package of upgrading data a library of the project has and DressCode lacks; --offline takes the catalogue of Packagist as the cache remembers it.
…uld move What of the lock refuses each target, and which packages raising the constraints would move across a major version. The lock is asked as composer why-not --locked answers offline, the moves as the dry run of composer require says, which --offline leaves out.
… for the team to edit The steps are what a human has to make way for a target, the bridge, the raise of Composer, the jump, the cleanup, the modernizations left out and, for an application, the update to the newest versions the constraints allow, written into dresscode-upgrade.neon with the commands of the tests, each writing a JUnit report; a library, told by its type and its lock, asked about in a terminal, or named by --library and --no-library, widens its constraints to the targets instead and its plan is the bridge alone.
…m back upgrade abort takes back what an unfinished run changed. A run makes them topic by topic in the style the code keeps (Upgrade\PlanRunner), a step whose tests newly fail going back byte for byte, and tells where it stopped. Whether a step is done is read from the code and the lock alone, so a run goes on from a working tree a team committed, squashed or rebased. It waits at a step of Composer with the command to run, runs the whole suite after the jump and the last step, and warns of a working tree with changes of its own. A step resolves the names with what the namespaces of the scope declare, as the estimate does. What a run keeps lies in the directory of the git repository of the project (Upgrade\RunState), from which upgrade abort restores the files of an interrupted step. A run is written as text or as a JSON document with how far it got.
…w process It shows what the update changes first, and goes on with the jump in the new process. In a terminal after asking or with --yes. A raise no fix follows is tested after it all the same, and upgrade abort restores the composer.json and the lock a raise changed too.
A dresscode:ignore with its reason confirms one.
…and one of the raise
… human In the code as it stands, and the suppressions about an upgrade giving no reason, exiting with 1 where one is left.
…eeps to go back, running no fix
… the workers included `--profile <file>` writes as JSON where the time of a run went, in nanoseconds: the phases (the start of the process, the start of the container of PHPStan or of a derived one and its scopes, the parse, the passes, the gaps, the print, the cycle collection, the run as a whole), every rule by the time of its callbacks, how often they ran and how often they changed the tree, every gap rule by the time of its claims, every file with its size, rounds and passes, and the peak memory of every process. A worker measures its own files and hands that over with every result and the parent adds it up, so the totals do not depend on the number of workers. A run with the option leaves the result cache out, a file served from the cache having nothing to measure. It says where the time of a run goes, not only how much of it: a comparison of two revisions then names the rule or the claim that moved. Without the option the engine only asks whether there is a profiler, and wraps a claim in a measurement only when there is one.
…voke(), the rule renamed to invokeCallNotation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AryXexSEtuxAHLeD4fZfQ8
ec58046 to
bf94e6f
Compare
|
Thanks for this, David. The implementation is careful: the rewrite held up on every edge case I threw at it, and the IDE problem you describe is a fair one to want solved. Still, I've decided not to take
It fits well as a rule of your own project, though, where #[RuleInfo(Stage::Structure, typesRequired: true, analyses: [Types::class])]
final class InvokeCallRequiredRule extends NodeRuledeclaring typeAnalysis: phpstan
rules:
- Acme\Style\InvokeCallRequiredRule
hygiene:
__invokeCall: keep
project:
invokeCall: requiredIt produced the same output as your PR. The So I'm closing this one, with thanks for the work and for the clear description. |
hygiene.__invokeCall: required, calling an invokable object through
__invoke()hygiene.__invokeCalltakes onlyforbiddenandkeep. This PR adds the opposite state,required:Motivation. With an explicit
__invoke(), go to definition and find usages in the IDE reach the__invoke()declaration, and a text search finds the calls of a handler.$handler($x)gives neither.What changes
hygiene.__invokeCallbecomesDomain::state('forbidden', 'required'), in the same shape asShortTernaryRule.NoExplicitInvokeCallsRuleis renamed toInvokeCallNotationRule(a…Notationrule writes one form of a family). Its fixtures move with it.forbiddenis unchanged. Its output is byte for byte the same, and the existing fixtures pass without edits.requiredneeds the types. A call is rewritten only whereAnalyses\Typesshows the callee to be an object for sure, every class it may be has__invoke(), and none of them isClosure. WithouttypeAnalysis: phpstanthe state reports nothing. The rule is nottypesRequired, becauseforbiddendoes not need the types.callable, a string or an array: it may be any kind of callable, and->__invoke()would fail on most of themClosure, and a union containing it: the__invoke()of a Closure leads to no declaration in the project, so it gains nothing$f(...): it makes a Closure of the object, not a callstrlen($x)($this->h)are dropped, and(new Handler)keeps its own. Multi-line arguments and their comments stay as written.__invoke(), so nothing new maps torequired.SlevomatCodingStandard.PHP.DisallowDirectMagicInvokeCallstill maps toforbidden.docs/dresscode.annotated.neonlistsrequired.composer referenceandcomposer apiwere regenerated.Tests
invokeCallNotation/requiredwith stubs: a variable, a property in parentheses,new, the result of a method call and of a static one, a union of two invokables, a call nested in the arguments, multi-line arguments with a comment, and the comment guard. It also covers the untouched cases:Closure,callable, an unknown type, a string, a class without__invoke(), a nullable object, a union withClosure,$f(...)and an existing->__invoke().composer phpstanis clean andbin/dresscode checkis clean.composer tester: 5 tests fail locally, all because my local PHP lackssimplexml(Interop/FormatterConfig,Interop/Translator,Upgrade/PlanRunner,Console/init,Console/Application). In aphp:8.4-clicontainer with SimpleXML,InteropandConsole/initpass. The remaining two fail there for reasons of the container itself (a git hook refusing the commit, running as root) and have nothing to do with this rule.🤖 Generated with Claude Code