Skip to content

One type per member, resolved where code names are resolved - #516

Draft
Firehed wants to merge 14 commits into
mainfrom
step/48
Draft

One type per member, resolved where code names are resolved#516
Firehed wants to merge 14 commits into
mainfrom
step/48

Conversation

@Firehed

@Firehed Firehed commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Build-manifest step-48. Docblock types now flow through the same place code
names are resolved. Every consumer of MethodInfo, PropertyInfo,
ConstantInfo, FunctionInfo, or ParameterInfo reads one Type that
already reconciles the native declaration with any docblock refinement — no
consumer knows or cares where each part came from.

The docblock grammar is not parsed in-house. phpstan/phpdoc-parser is
promoted to a direct dependency, Parser\DocblockTypeAnnotator runs
alongside NameResolver and stores a resolved TypeNode per tag on the
declaring node, and TypeFactory::fromDocblockType + TypeFactory::merge
turn that into the merged Domain\Type.

Done clauses

  • Ledger's docblock types row lists DocblockTypeAnnotator and
    ResolvedSymbolPresenter as holders and keeps ExpressionResolver
    pending step-49 (tests/Architecture/OneRoutePerFactTest.php).
  • Ledger's php-parser name resolution row gains
    DocblockTypeAnnotator as a holder (same test).
  • @return list<User> method's returnType->valueType() is the fully
    qualified User
    (DefaultClassInfoFactoryTest::testMethodReturnListOfUserResolvesValueTypeToFullyQualifiedUser).
  • TypeConstructionRule still passes (composer test).
  • FunctionSurfaceParityTest golden unchanged (composer test).

Also amends step-48 and step-49 rows so arrayElementType's deletion lands
with its sole caller in step-49 (small correction to the manifest wording;
first commit on the branch).

The ClassLikeLookupParityTest golden gains merged types on PSR
ServerRequestInterface methods where the native declaration was untyped —
recaptured in the class-factory commit; small diff, visible improvement.

Firehed and others added 11 commits September 9, 2026 10:33
Move arrayElementType deletion from step-48 to step-49 (its caller is
only replaced in step-49; step-48 cannot delete a method whose one
caller still lives). Switch step-48's docblock parser from an in-house
string parser to phpstan/phpdoc-parser and record the Tighten it adds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Docblocks are parsed text; the Parser tier is where parsing lives. The
Resolution layer already depends on Parser, so both call sites
(ExpressionResolver, ResolvedSymbolPresenter) keep working through the
existing layer edge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Already present transitively (via phpstan). DocblockTypeAnnotator will
parse docblock types through it; promote to a direct dep so the version
constraint is visible in composer.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Turn a resolved phpstan/phpdoc-parser TypeNode into a Domain\Type.
Every identifier the annotator hands us is already either a
PrimitiveType name or a fully qualified class name, so no name
resolution runs here. Unsupported shapes (callables, conditionals,
array shapes) yield null so callers keep the native type unchanged.

Confine PHPStan\PhpDocParser\* to TypeFactory. The annotator will be
added to the allowlist when it exists.

Expose PrimitiveType::$name for the merge routine that follows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
One reconciliation point so no downstream code has to know a type's
origin. Native declarations win on the outer shape; a docblock only
fills in the value type when the native says `array` or `iterable`,
or names the same class without generics of its own.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
One visitor reads @var, @return, @PARAM (plus psalm/phpstan spellings) of
every ClassMethod, Property, ClassConst, and Function_. It parses the
text through phpstan/phpdoc-parser, walks the resulting TypeNode tree,
and rewrites every class-name IdentifierTypeNode into its fully
qualified form through the same PhpParser\NameContext the sibling
NameResolver populates. The resolved TypeNodes land as the
resolvedDocblockTypes attribute keyed by tag (`return`, `var`,
`param:<name>`), which the FunctionInfo and ClassInfo factories will
read next.

Adds a phpstan disallowedNamespaces rule confining PHPStan\PhpDocParser\*
to the annotator and TypeFactory, and updates the ledger: the
name-resolution row gains DocblockTypeAnnotator; docblock-types swaps
TypeFactory for DocblockTypeAnnotator among its holders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Attach the annotator alongside NameResolver, sharing its NameContext.
Every parsed tree now carries resolvedDocblockTypes on every
ClassMethod, Property, ClassConst, and Function_ that has a docblock.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Read the resolvedDocblockTypes attribute set by DocblockTypeAnnotator
and reconcile each part through TypeFactory::merge. A new
TypeFactory::fromDocblockAttribute helper keeps the attribute's shape
(a map from tag key to phpdoc-parser TypeNode) hidden inside the
Domain layer, so callers reason in Type only. ParameterInfo::fromNode
accepts an optional pre-computed docblock Type so a caller building
parameter list at the function-info site drives the merge without
touching phpdoc-parser directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every AST path — method, plain property, promoted constructor property,
class constant — now reads its declaring node's resolvedDocblockTypes
attribute and reconciles the native declaration with the docblock
through TypeFactory::merge. Downstream MethodInfo, PropertyInfo, and
ConstantInfo objects hold one Type with no awareness of origin.

Recapture the class-like-lookup golden: PSR ServerRequestInterface's
untyped-in-native parameters (`$default`, `$value`, `$data`, and the
`getParsedBody` return) now surface the mixed/null|array|object types
their docblocks declare.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prove the Done clause end-to-end: parse UserRepository through the
production TreeAnnotator (NameResolver + DocblockTypeAnnotator), pass
the node to DefaultClassInfoFactory, and assert the resulting
MethodInfo::returnType->valueType() is a ClassName of the fully
qualified Fixtures\Domain\User.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.44262% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.23%. Comparing base (4c296ef) to head (1fa0c29).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/Domain/TypeFactory.php 88.15% 9 Missing ⚠️
src/Parser/DocblockTypeAnnotator.php 96.42% 2 Missing ⚠️
src/Domain/FunctionInfo.php 90.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #516      +/-   ##
============================================
- Coverage     99.45%   99.23%   -0.22%     
- Complexity     1897     1966      +69     
============================================
  Files           134      135       +1     
  Lines          4954     5121     +167     
============================================
+ Hits           4927     5082     +155     
- Misses           27       39      +12     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@Firehed
Firehed marked this pull request as ready for review September 9, 2026 19:38
@Firehed
Firehed marked this pull request as draft September 9, 2026 19:40
The visitor is one class check plus one identifier check; every other
TypeNode is descended into by the library traverser. A new TypeNode
subclass in phpdoc-parser gains identifier resolution automatically.
Readers pass a typed php-parser Node and TypeFactory owns the mixed-shape
attribute boundary. No caller carries a mixed value through its signature.
Discovers every concrete PHPStan\PhpDocParser\Ast\Type\* implementing
TypeNode and asserts each is listed as SUPPORTED or UNSUPPORTED. A new
subclass in a library release fails the test so a human decides which
bucket it belongs to, rather than silently becoming null in
TypeFactory::fromDocblockType.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant