A PHP parser, code-style checker, and project-aware static analyzer written in Go.
go-php-parser turns PHP source into a detailed Abstract Syntax Tree, checks it against a registered set of style rules (PSR-12 and friends), and runs a project-aware analyzer that resolves symbols, types, and control flow across the configured files. Diagnostics are emitted in deterministic source order with stable exit codes, and the same engine backs the analyze command and the PHP Strom language server.
The long-term target is a production-grade, full PHP static analyzer. Current implementation work is diagnostic correctness and false-positive reduction against PHPStan-gated fixtures and reviewed corpora. Cold full-project performance comparable to Mago remains a later release gate, not the active queue. See Full Static Analyzer and Mago-Class Performance Target for the current pin, M1 status, ranked next actions, and acceptance gates. Remaining CLI adoption work is in Near-term CLI and adoption plan and is not the main stream.
- PHP 8+ syntax
- Function declarations with parameters
- Variable declarations and assignments
- Control structures (if, elseif, else)
- String literals (single and double quoted)
- String interpolation
- Integer and float literals
- Boolean literals (true, false)
- Null literal
- Comments (single-line and doc comments)
- Basic expressions and operators
- Detailed position tracking (line, column, offset)
- Hierarchical node structure
- Support for:
- Function nodes
- Variable nodes
- Parameter nodes
- Assignment nodes
- Expression nodes
- Control structure nodes
- Comment nodes
- Literal nodes (string, integer, float, boolean, null)
git clone https://github.com/yourusername/go-php-parser.git
cd go-php-parser
go mod downloadBuild the binary once:
make buildThis produces a binary named go-phpcs.
The binary auto-discovers a config in the current working directory, in this order:
tusk.yamlgo-phpcs.yamlgo-phpcs.ymlconfig.yaml
To bootstrap a fresh project, generate a default config.yaml and edit it:
./go-phpcs initYou can also place the binary alongside an existing config.yaml from this repo and edit it to target the directory you want to check. The binary will pick up the nearest config automatically.
To inspect which config, path, extensions, ignore list, rules, and analysis level are actually in effect, run:
./go-phpcs configTo print exactly which files the resolved config will scan:
./go-phpcs list-files./go-phpcsOptionally export the report into a file:
./go-phpcs -o report.logClone your project into a folder within this project (for example demo_project/).
Update config.yaml with your folder name.
Run the style checks:
make runRun project-aware static analysis for the files selected by config.yaml:
./go-phpcs analyzeOr analyze one file using the same project pipeline:
./go-phpcs analyze src/Example.phpOr scope the analysis to a subfolder (the whole project is still indexed for cross-file symbol resolution, but only files under src/Module are reported on):
./go-phpcs analyze src/ModuleThe folder walk respects the configured extensions and ignore lists, so ./go-phpcs analyze src and a config pointed at src produce the same file set.
Set analysis_level to zero or greater to run rules up to that level. Leaving it unset runs every registered analysis rule.
path: ./src
extensions:
- php
analysis_level: 0The analyzer parses each selected file once, builds one immutable project snapshot, and emits diagnostics in deterministic source order. Exit code 0 means clean, 1 means analysis or parser findings, and 2 means an invocation, configuration, discovery, or file-read failure.
The counts below are registered engine rules, not PHPStan error-identifier counts. A single engine rule can cover several PHPStan identifiers through one shared traversal. “Cumulative” reflects the rules enabled when analysis_level is set to that level. Unlevelled rules run only when analysis_level is omitted.
| PHPStan level | Rules introduced | Cumulative levelled rules | Detail |
|---|---|---|---|
| 0 | 2 | 2 | Level 0 rules |
| 1 | 1 | 3 | Level 1 rules |
| 2 | 15 | 18 | Level 2 rules |
| 3 | 5 | 23 | Level 3 rules |
| 4 | 1 | 24 | Level 4 rules |
| 5 | 1 | 25 | Level 5 rules |
| 6 | 5 | 30 | Level 6 rules |
| 7 | 1 | 31 | Level 7 rules |
| 8 | 1 | 32 | Level 8 rules |
| 9 | 1 | 33 | Level 9 rules |
| 10 | 0 | 33 | Level 10 rules |
| Unlevelled | 4 | 37 total registered | Unlevelled rules |
Run go run ./cmd/rule-inventory after adding or moving an analysis rule. The Go test suite compares this table and each detail page's inventory metadata with the live registry, so a rule-count change cannot land without updating both. The linked level documents describe current coverage and known boundaries; update that prose in the same change as its rule or level.
You can list all available style rule codes supported by this tool using the list-style-rules command. This is useful for discovering which rules you can enable or disable in your config.yaml.
Run the following command:
./go-phpcs list-style-rulesThis will print a list of all registered style rule codes, for example:
Available style rule codes:
PSR12.Files.EndFileNoTrailingWhitespace
PSR12.Files.EndFileNewline
PSR12.Files.NoMultipleStatementsPerLine
PSR12.Files.NoSpaceBeforeSemicolon
PSR12.Files.NoBlankLineAfterPHPOpeningTag
PSR12.Classes.OpenBraceOnOwnLine
PSR12.Methods.VisibilityDeclared
PSR1.Classes.ClassDeclaration.PascalCase
PSR12.Classes.ClosingBraceOnOwnLine
...
You can then copy any of these codes into your config.yaml under the rules: section to customize which checks are performed.
This parser implements several PSR-12 style checks, including:
- No trailing whitespace (
PSR12.Files.EndFileNoTrailingWhitespace): Disallows trailing whitespace at the end of lines. - File must end with a single blank line (
PSR12.Files.EndFileNewline): Ensures files end with exactly one blank line. - No multiple statements per line (
PSR12.Files.NoMultipleStatementsPerLine): Disallows more than one statement (semicolon) per line. - No space before semicolon (
PSR12.Files.NoSpaceBeforeSemicolon): Disallows any space or tab before a semicolon at the end of a statement. - No blank line after opening <?php tag (
PSR12.Files.NoBlankLineAfterPHPOpeningTag): Disallows blank lines immediately after the opening PHP tag. - Class opening brace on its own line (
PSR12.Classes.OpenBraceOnOwnLine): Requires that the opening brace for a class, interface, trait, or enum must appear on its own line, with no leading or trailing whitespace. - Method visibility must be declared (
PSR12.Methods.VisibilityDeclared): Requires that every class method explicitly declares its visibility (public,protected, orprivate).
Style issues are reported per file and line, and can be extended by adding new checkers in the style/ package.
You can enable or disable specific code style rules using the rules: key in your config.yaml. If no rules are specified, all available rules are run.
List of Available Rules:
PSR12.Files.EndFileNoTrailingWhitespacePSR12.Files.EndFileNewlinePSR12.Files.NoMultipleStatementsPerLinePSR12.Files.NoSpaceBeforeSemicolonPSR12.Files.NoBlankLineAfterPHPOpeningTagPSR12.Classes.OpenBraceOnOwnLinePSR12.Methods.VisibilityDeclaredPSR12.Classes.ClosingBraceOnOwnLine
| Rule Code | Description |
|---|---|
| PSR12.Files.EndFileNoTrailingWhitespace | Enforces no trailing whitespace on lines |
| PSR12.Files.EndFileNewline | File must end with a single blank line |
| PSR12.Files.NoMultipleStatementsPerLine | Disallows more than one statement (semicolon) per line |
| PSR12.Files.NoSpaceBeforeSemicolon | Disallows any space or tab before a semicolon at the end of a statement |
| PSR12.Files.NoBlankLineAfterPHPOpeningTag | Disallows blank lines after the opening <?php tag |
| PSR1.Classes.ClassDeclaration.PascalCase | Enforces PascalCase for class names |
| PSR12.Classes.ClosingBraceOnOwnLine | Closing brace must be on its own line, and not followed by code or comments. Reports a syntax error if the file contains only a closing brace |
Example config.yaml:
path: ./src
extensions:
- php
ignore:
- vendor
rules:
- PSR12.Files.EndFileNoTrailingWhitespace
- PSR12.Files.EndFileNewline
- PSR12.Files.NoMultipleStatementsPerLine
- PSR12.Files.NoSpaceBeforeSemicolon
- PSR12.Files.NoBlankLineAfterPHPOpeningTag
- PSR1.Classes.ClassDeclaration.PascalCase
- PSR12.Classes.ClosingBraceOnOwnLineAdd or remove rule codes under rules: to control which checks are performed. If you don't specify rules it will execute all rules available.
go run main.go demo_projectThis will parse the PHP files under the target directory and output the AST in a tree-like structure. You can also point it at a single file:
go run main.go demo_constants.phpYou can scan all PHP files in a directory as defined in config.yaml:
go run main.goTo control parallelism (number of concurrent workers), use the -p flag. By default, the number of workers is set to the number of CPU cores on your machine:
go run main.go -p 4 # Use 4 workers in parallelFirst, fetch the pinned corpora (not committed to this repository — see test_projects/manifest.json):
go run ./cmd/fetch-test-projectsTo track parser compatibility progress across the checked-in corpus under test_projects, run:
make compat-metricsThis prints overall file compatibility, per-project compatibility, total parse errors, and a small sample of the first failing files per project.
You can also emit a machine-readable snapshot for tracking over time:
go run ./cmd/compat-metrics -json -output compatibility-report.jsonUseful flags:
-rootto scan a different corpus root-workersto control parallelism-topto control how many failing-file examples are shown per project
Parser compatibility and PHPStan diagnostic compatibility are separate metrics. To report a corpus-scoped Level N: X% PHPStan compatible, use the exact-location precision/recall/F1 report documented in docs/phpstan-compatibility-metric.md:
go run ./cmd/phpstan-compat \
--root /path/to/project \
--paths src,tests \
--index-paths vendor \
--phpstan-bin /path/to/project/vendor/bin/phpstan \
--phpstan-config /path/to/project/phpstan.neonTo measure the analysis engine itself (not the style checker) against the checked-in test_projects corpus — index-only, process-cold full analysis, and warm-loop full analysis, with timing, RSS, and diagnostic counts per the full-static-analyser benchmark contract:
go run ./cmd/benchmark --root test_projects/symfony --json --output benchmark-report.jsonOr a human-readable summary:
go run ./cmd/benchmark --root test_projects/phpunitFor a selected-path workload, pass the same source/include boundary used by the reference analyser. Paths are relative to --root; missing paths fail instead of silently shrinking the corpus:
go run ./cmd/benchmark \
--root test_projects/wordpress-develop \
--paths src,tests,vendor \
--excludes src/js \
--jsonCold-full-analysis runs each re-exec the binary as a fresh subprocess (10 by default) so no in-process cache state leaks between measured runs. The parent times the entire child lifetime, including startup, discovery, reads, parsing, indexing, analysis, reduction, and result serialization. Warm-full-analysis loops the indexed analysis pipeline in a single process after one unmeasured warmup iteration. Incremental-edit timing is reported as unsupported — the engine has no incremental invalidation API yet.
test_projects/* (other than manifest.json) are fetched on demand, not committed — each is large (tens to hundreds of MB) and Git has no reliable way to pin an external directory's exact revision without either committing its full content or a real submodule. go run ./cmd/fetch-test-projects reads test_projects/manifest.json and checks out each project's exact pinned commit (a shallow, single-commit fetch, not a full clone) into test_projects/<name>, skipping projects already at the pinned commit. The manifest records the Mago benchmark's three required workloads (php-standard-library, wordpress-develop, magento2) alongside this project's own representative framework corpora (Composer, Drupal, Laravel, PHPUnit, Symfony), each with its exact commit per the comparable-performance contract.
go run ./cmd/fetch-test-projects # fetch everything in the manifest
go run ./cmd/fetch-test-projects --only psl,magento2 # fetch a subset
go run ./cmd/fetch-test-projects --force # re-fetch even if already at the pinned commitTo re-pin a project to a newer revision, update its commit (and ref, for readability) in test_projects/manifest.json and re-run with --force.
Useful flags:
--rootcorpus root to scan--pathscomma-separated paths within the root to scan--excludescomma-separated paths within the root to exclude--levelanalysis rule level filter (-1= run every registered rule)--cold-runsnumber of measured process-cold runs (contract minimum is 10)--warm-iterationsin-process warm-loop iterations, including the unmeasured warmup--skip-coldskip the process-cold subprocess runs for a quick check--cpuprofile/--memprofilewrite ago tool pprof-compatible CPU or heap profile from a single in-process full-analysis run (bypasses the cold/warm harness so the profiler attaches directly to the profiled work); pair with--profile-iterationsto profile several in-process passes at once
After scanning, the tool will print performance statistics:
Scan completed in 1.55 seconds
Total lines scanned: 1653877
Lines per second: 1063784.86
Total parsing errors: 0
HeapAlloc: 148.56 MB
Sys: 298.92 MB
File scanning is controlled by config.yaml:
path: ./demo_project
extensions:
- php
ignore:
# - vendorpath: Directory to scanextensions: File extensions to includeignore: Directories to skip (uncomment to enable)
package main
import (
"go-php-parser/lexer"
"go-php-parser/parser"
"go-php-parser/ast"
)
func main() {
// Read PHP file
input := `<?php
function test($param) {
echo "Hello, $param!";
}`
// Create lexer
l := lexer.New(input)
// Create parser
p := parser.New(l)
// Parse the input
nodes := p.Parse()
// Check for errors
if len(p.Errors()) > 0 {
fmt.Println("Parsing errors:")
for _, err := range p.Errors() {
fmt.Printf("\t%s\n", err)
}
return
}
// Print AST
ast.PrintAST(nodes, 0)
}go-php-parser/
├── ast/ # AST node definitions
├── lexer/ # Tokenizer implementation
├── parser/ # Parser implementation
├── token/ # Token type definitions
├── examples/ # Example PHP files
└── main.go # Main entry point
Node- Base interface for all AST nodesPosition- Line/column/offset information
Identifier- Variable or function namesVariableNode- PHP variables ($var)StringLiteral- String literalsInterpolatedStringLiteral- Strings with variable interpolationIntegerLiteral- Integer literalsFloatLiteral- Floating-point literalsBooleanLiteral- Boolean literals (true/false)NullLiteral- Null literalBinaryExpr- Binary expressionsFunctionCall- Function calls
FunctionNode- Function declarationsParameterNode- Function parametersAssignmentNode- Variable assignmentsExpressionStmt- Expression statementsReturnNode- Return statementsIfNode- If statementsElseIfNode- Elseif clausesElseNode- Else clausesWhileNode- While loopsCommentNode- Comments
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.