for PHP that finds potential bugs without running your code • Uses type declarations to catch errors early: undefined variables and methods, type mismatches, and more • Choose how strict to be with levels 0–10
reading • Instead of understanding the details (micro) first, grasp the big picture (macro), then read the real thing • Build something “less than PHPStan” as a roadmap for reading the real thing
Know What to Check Does that method exist? Read the Structure Variable assignments, method calls Read Through Top to bottom, tracking each variable Apply the Checks Report When a method is called, check it “That class doesn't have that method!”
ministan Files ~2,000 8 Lines (excl. blank lines, etc.) ~200,000 220 ministan still has a fair amount of code, so in this talk I'll explain it so you can get a rough feel for it
code is just a sequence of characters • Humans just skim back and forth, and it works • For a machine to read it, you need fixed rules • Break the code into “meaningful chunks”
Syntax Tree • Split code into “meaningful chunks” = “nodes”, arranged as a tree • Nodes come in kinds (assignment, method call, variable…) • Things irrelevant to meaning, like “;” and whitespace, are thrown away • PHPStan builds the AST with php-parser (nikic/php-parser)
Implement rules like the one we saw in the intro • List the rules you want to enable in an array • Pass $ast in the constructor so a rule can look up definitions (functions, classes) • What a concrete Rule looks like comes later
one node and check it with the rules matching its kind • Receive the rules to use through the constructor • It runs once per node, so instead of printing errors right away, collect them and “Report” them all at the end • It actually runs during “Read Through”
AST from parent to child, top to bottom • The same motion as the “read line by line” we humans did • Every time we reach a node, call “Apply the Checks”
myfunc('xxx') Function call Class::method() Static method call on a class $this->method() Call via $this inside a class $some->method() Method call via a variable
myfunc('xxx') Function call Class::method() Static method call on a class $this->method() Call via $this inside a class $some->method() Method call via a variable
myfunc('xxx') Function call Class::method() Static method call on a class $this->method() Call via $this inside a class $some->method() Method call via a variable
myfunc('xxx') Function call Class::method() Static method call on a class $this->method() Call via $this inside a class $some->method() Method call via a variable
The AST only has the name “this” — the class name isn't written anywhere • What the pseudo-variable $this holds depends on the context • We need someone in charge of remembering what $this held
myfunc('xxx') Function call Class::method() Static method call on a class $this->method() Call via $this inside a class $some->method() Method call via a variable
• Before • The original Scope was only passed down “parent → child” • After • Return the processed Scope and hand it over to the next step • But what's learned inside a class isn't carried outside
small (at express speed) • ~200,000 lines → 220 lines • Even shrunk down, the flow and the cast (the skeleton) stayed the same • That said, plenty of corners were cut • Use ministan as a stepping stone: “what's different in the real one?” • Even the rules you use daily show real craft and depth once you read them
php-parser is used by many PHP tools, such as Rector and Psalm • ministan also uses php-parser as-is • If you only need “does the class have this method?”, couldn't PHP's built-in Reflection tell you?
it” • Reflection needs you to require (= execute) the file • If definitions and logic share a file, the logic runs before you can inspect • ministan also tried Reflection first, hit exactly this, and switched to finding definitions in the AST • The real PHPStan moved to “static reflection” (reading definitions from the AST) in 0.12.26 (BetterReflection) • See: https://phpstan.org/blog/zero-config-analysis-with-static-reflection
• More code means more nodes, and more rules means even more loops. The repetition multiplies • Picture your own project… hundreds of files × hundreds of nodes × hundreds of rules • The real one also does type inference (computing Scope) for every node
• Pick out only the rules relevant to that node (and cache the result) • Split files into “jobs” of 20 and analyze them in parallel across multiple worker processes • Skip analyzing files that haven't changed (result cache) • And recently: a C++ extension (PHPStan Turbo), and using pcntl_fork() to skip worker startup
in? • In ministan, I listed the rules by hand • Where does the real one line up the built-in rules and the custom rules you write? • And how do “levels” change which rules are enabled in the first place?
declare “from which level they're enabled” with an attribute • #[RegisteredRule(level: 0)] • Pick level N, and only rules with level ≤ N get registered • src/DependencyInjection/AutowiredAttributeServicesExtension.php • Where rules with the attribute are collected, filtered by level, and registered
a class name under rules: in phpstan.neon, and that rule gets registered • It goes into the same “rule list” as the built-in rules • So inside the real PHPStan, built-in and custom rules are treated the same way • src/DependencyInjection/RulesExtension.php • Where the classes listed under rules: are added to the rule list