Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Let's Build a Tiny PHPStan

Avatar for asumikam asumikam
October 03, 2026
67

Let's Build a Tiny PHPStan

Avatar for asumikam

asumikam

October 03, 2026

More Decks by asumikam

Transcript

  1. Find Bugs Without Writing Tests • A static analysis tool

    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
  2. You can write “custom rules” too • Besides the rules

    PHPStan ships with, you can create your own rules to enforce • Great for enforcing the rules of your domain
  3. With just this,*1 it actually applies the rule for you

    *1 In practice you also tweak phpstan.neon a little, but that's a minor detail
  4. • At first, I didn't even know where to start

    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
  5. Goal of This Talk Through my “Build It Small” version,

    people who don't know PHPStan's internals get a rough picture of how it works inside!
  6. Asumi (@asumikam) 🍊 • Linkage, Inc. • PHPer • Agile

    / Scrum • Organizer of PHP Conference Odawara
  7. #1 Build It Small 1. Look at the “Build It

    Small” implementation 2. Write some real rules
  8. #1 Build It Small 1. Look at the “Build It

    Small” implementation 2. Write some real rules
  9. #1 Build It Small Is there a rename method? Does

    it take exactly one argument?
  10. #1 Build It Small What I do in my head

    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!”
  11. #1 Build It Small What I do in my head

    Know What to Check Read the Structure Read Through Apply the Checks Report With these five as the backbone, let's “Build It Small”: a tiny PHPStan
  12. #1 Build It Small How small are we going? PHPStan

    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
  13. #1 Build It Small The file to analyze $ php

    bin/ministan AnalyzeTarget.php
  14. #1 Build It Small Let's take a look at ministan

    $ php bin/ministan AnalyzeTarget.php
  15. #1 Build It Small Building “Read the Structure” • Source

    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”
  16. #1 Build It Small AST (Abstract Syntax Tree) • Abstract

    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)
  17. #1 Build It Small All of these are nodes Stmt_Expression

    Expr_Variable Expr_New Name Expr_MethodCall Identifier Arg Scalar_String
  18. #1 Build It Small • Starts with an array of

    Stmt • Line 1 0: Stmt_Expression • Line 2 1: Stmt_Expression • Walking the AST parent → child = reading code line by line, top to bottom, outside in
  19. #1 Build It Small • Nodes contain more nodes (nesting)

    • Node • Array of nodes • Plain value
  20. #1 Build It Small Building “Know What to Check” •

    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
  21. #1 Build It Small Building “Apply the Checks” • Take

    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”
  22. #1 Build It Small Check with the rule (if it

    fails, a message comes back)
  23. #1 Build It Small Building “Read Through” • Walk the

    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”
  24. #1 Build It Small At first, the whole file's AST

    ($ast) is passed in $nodeCallback is the one we built in “Apply the Checks”
  25. #1 Build It Small 1. Look at the “Build It

    Small” implementation 2. Write some real rules
  26. #1 Build It Small “Does the function / method exist?”

    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
  27. #1 Build It Small “Does the function / method exist?”

    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
  28. #1 Build It Small I want an error when the

    function doesn't exist // <- HERE!!
  29. #1 Build It Small “Does the function / method exist?”

    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
  30. #1 Build It Small I want an error when the

    static method doesn't exist // <- HERE!!
  31. #1 Build It Small Get the class name & method

    name of the node being checked
  32. #1 Build It Small Get all the classes in the

    whole code → loop over them
  33. #1 Build It Small “Does the function / method exist?”

    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
  34. #1 Build It Small I want an error when the

    method doesn't exist on $this // <- HERE!!
  35. #1 Build It Small The mechanism needs an update •

    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
  36. #1 Build It Small Just remember it when we enter

    a class definition (node) “this” is “User” 🧠
  37. #1 Build It Small When we reach the code further

    down, just use the value we remembered “this” is “User” 🧠
  38. #1 Build It Small Pass $scope around as an argument

    So “Apply the Checks” can use it too
  39. #1 Build It Small While walking the AST, once we

    enter a class, the value is stored in Scope “this” is “User” 🧠
  40. #1 Build It Small The rest is the same as

    before: check whether it matches
  41. #1 Build It Small “Does the function / method exist?”

    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
  42. #1 Build It Small I want an error when $user

    doesn't have the method // <- HERE!!
  43. #1 Build It Small The mechanism needs an update Scope

    only remembered things when entering a class definition • • Just do the same thing on assignment
  44. #1 Build It Small What the slides didn't fully cover

    • 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
  45. #1 Build It Small Recap • Built PHPStan's internals, but

    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
  46. #1 Build It Small Disclaimer • ministan is focused on

    “Build It Small”, so it is intentionally released incomplete • Compare it with the real PHPStan and enjoy its imperfections
  47. #1 Build It Small ministan ↔ PHPStan Cheat Sheet PHPStan

    ministan ParserFactory Parser\ RichParser Rule-related Rules\ Rule, LazyRegistry FileAnalyserCallback Analyser\ FileAnalyserCallback NodeScopeResolver Analyser\ NodeScopeResolver Scope Analyser\ MutatingScope NodeFinder Reflection\ ReflectionProvider echo Command\ErrorFormatter\ ErrorFormatter
  48. #2 Compare with PHPStan Comparing with PHPStan • ministan works,

    but it's “less than PHPStan” • So: “How does the real one do it?” • Why use an AST? • How is it so fast? • How are custom rules wired in?
  49. #2 Compare with PHPStan ① Why use an “AST”? •

    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?
  50. #2 Compare with PHPStan ① Because “loading it means running

    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
  51. #2 Compare with PHPStan ② How is it so fast?

    • 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
  52. #2 Compare with PHPStan ② The secrets of its speed

    • 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
  53. #2 Compare with PHPStan ③ How are custom rules wired

    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?
  54. #2 Compare with PHPStan ③-1 Registering levels • Built-in rules

    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
  55. #2 Compare with PHPStan ③-2 Registering custom rules • Write

    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
  56. Goal of This Talk Through my “Build It Small” version,

    people who don't know PHPStan's internals get a rough picture of how it works inside!
  57. Knowing what's inside the tools you use every day lets

    you do more and makes them less scary