Skip to content

feat(compat): emit field/enum_member info for PHP, Python, Ruby#53

Merged
gjtorikian merged 1 commit intomainfrom
feat/extractors-emit-fields
May 2, 2026
Merged

feat(compat): emit field/enum_member info for PHP, Python, Ruby#53
gjtorikian merged 1 commit intomainfrom
feat/extractors-emit-fields

Conversation

@gjtorikian
Copy link
Copy Markdown
Collaborator

Why

#51 shipped baseline-walking rename detection in diffSnapshots but couldn't bring PHP/Python/Ruby to 0 breaking on workos/openapi-spec#17 because the structural info those extractors produce isn't sufficient to pair renamed types:

  • Python's @dataclass model classes were categorized as ApiClass (because they have from_dict/to_dict helper methods) — losing field info.
  • PHP's PHP 8 model classes use constructor-promoted properties (readonly class Foo { function __construct(public Type $field, …) {} }). The parser only accepted simple_parameter parameter nodes and skipped property_promotion_parameter entirely — promoted properties weren't captured anywhere.
  • Ruby's enums are emitted as classes (class ApplicationsOrder; ASC = "asc"; …; ALL = [...].freeze; end), not modules. The enum extractor only walked module nodes, so class-shape enums fell through to extractClasses and surfaced as ApiClass with no enum_member children.

Without field/enum_member symbols, the rename detector's structural matcher had nothing to compare on.

What

PHP (php-parser.ts, php-surface.ts):

  • Extended parseMethods's parameter walker to accept property_promotion_parameter nodes alongside simple_parameter. When a parameter carries a visibility modifier, surface it under PhpClass.properties so the surface builder can read it.
  • Added isValueObjectClass predicate: a non-exception, non-enum class with at least one public promoted property is a value object → emit as ApiInterface with promoted-property fields.

Python (python-surface.ts):

  • Added isDataclass predicate (matches @dataclass, @dataclass(), @dataclass(slots=True), etc. via the parser's existing decorators array).
  • New category 4b in the surface builder: @dataclass classes with at least one field → ApiInterface regardless of method presence. The field set is the canonical identity for data-shape classes.

Ruby (ruby-parser.ts):

  • Added isEnumShapedClass predicate: a class node whose body has only constant declarations (no method/singleton_method/call-shaped children — that filter naturally excludes attr_accessor + the ALL = [...].freeze aggregator since extractEnumConstants only collects scalars).
  • extractEnumModules also walks class nodes through this predicate.
  • extractClasses skips enum-shaped classes via the same predicate to prevent double-emission.

differ.ts:

  • Extended bareTypeName with a PHP namespace-prefix strip (\Vendor\Pkg\FooFoo). Without this, PHP method return_type_changed reports come in as \WorkOS\Resource\ApiKeyWithValue and don't match the rename map's bare-name keys.

End-to-end validation on workos/openapi-spec#17

Re-extracted baseline + candidate locally with the new extractors, then ran oagen compat-diff:

Language Before everything After #49 After #51 After this PR
dotnet 21 0 0 0
go 9 1 0 0
php 11 7 7 0
python 11 3 3 0
ruby 13 2 2 0

All 33 originally-reported breaking changes are now soft-risk. The PR-comment compat report on workos/openapi-spec#17 will collapse to Breaking 0 / Soft-risk ~80 once oagen ships and openapi-spec bumps its dep.

Tests

Existing extractor + differ tests pass unchanged (1381/1381). No new tests added in this commit — the existing test fixtures already cover the categorization branches I extended; the regressions guarded by tests were that BaseRequestException (Python/PHP exception with public properties) doesn't get misclassified as a value object, which the existing extracts exception classes tests cover and pass.

If you'd like dedicated tests for the new branches (constructor-promoted PHP class → ApiInterface, dataclass with helper methods → ApiInterface, Ruby class-shape enum), I can add them as a follow-up — happy to take that direction in review.

Test plan

  • npm test — 1381/1381
  • npm run lint clean
  • npx tsc --noEmit clean
  • npm run build clean
  • End-to-end: regenerated workos-php / workos-python / workos-ruby SDKs against the new spec, extracted baseline + candidate, ran oagen compat-diff for all 5 languages — all 0 breaking

🤖 Generated with Claude Code

PR #51's rename-detection needs field/enum_member info to pair renamed
types and canonical-flipped enums. Three extractors weren't emitting it.

PHP: PHP 8 model classes use constructor-promoted properties. Parser
only accepted simple_parameter, skipped property_promotion_parameter.
Extended walker to recognize promoted properties; added
isValueObjectClass to classify them as ApiInterface.

Python: @DataClass classes with from_dict/to_dict helpers fell through
to ApiClass, losing field info. Added explicit dataclass branch that
keeps them as ApiInterface regardless of method presence.

Ruby: enums emit as classes not modules. Extended extractEnumModules to
also walk constants-only class nodes, with extractClasses skip to
prevent double-emission.

Also: bareTypeName now strips PHP namespace prefix
(\Vendor\Pkg\Foo → Foo) so the cascade matches qualified return
types.

End-to-end on workos/openapi-spec#17 — all five languages now report 0
breaking changes (php 7→0, python 3→0, ruby 2→0; go and dotnet
already 0).

Tests: 1381/1381 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gjtorikian gjtorikian merged commit 6a21ac1 into main May 2, 2026
5 checks passed
@gjtorikian gjtorikian deleted the feat/extractors-emit-fields branch May 2, 2026 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant