Skip to content

fix: sharp/bin/spec writes each type of an example to a file of its own - #147

Merged
heyJordanParker merged 1 commit into
masterfrom
fix/spec-file-per-type
Oct 16, 2026
Merged

heyJordanParker merged 1 commit into
masterfrom
fix/spec-file-per-type

Conversation

@heyJordanParker

Copy link
Copy Markdown
Owner

Fixes: sharp/bin/spec writes every type of an example, its setup and its example class into one generated .sharp file, so the one-type-per-file rule of section 27 would refuse 26 examples that pass today.

This PR changes only the measure, as sharp/bin/spec changes in a PR of its own. The rule itself comes in the section 27 feature PR.

Problem

Section 27 says a .sharp file that declares a type declares one, named like the file. build_file wrote one file per block: the block's types, the types of the marker's setup, and the Example or in <Class> wrapper class. 29 examples write several types into that one file:

  • one-type-per-file, on purpose.
  • one-body-per-method and sub-interface-defaults, in their own text, which the feature PR splits.
  • 26 others, through their setup or the wrapper class. listeners is one: its setup declares the interface OrderEvent and the events OrderPaid and OrderRefunded beside its class OrderNotices.

Solution

  • build_files returns one file per type. split_types leads each file with the block's namespace and import lines. Each type goes to the path its namespace and name give, as every other file already did. What is no type, such as an extern line or a module block, stays with the first type, which the rule allows. A comment above a type goes with it. Each file keeps its own line map, so every reported line is still the spec's.
  • A block whose first line is a path comment, such as // app/Tenant/Store/StoreService.sharp, stays one file as written, so one-type-per-file still shows its error.
  • variant_files stops the run when two files of one example have one path. The rename to Name1.sharp is gone, because it hid two types of one name. No example has two files at one path today.

listeners now compiles four files: app/OrderEvent.sharp, app/OrderPaid.sharp, app/OrderRefunded.sharp and app/OrderNotices.sharp.

sharp/spec-self-test.md gains file-per-type. Its setup declares Ticket and Seat, and an error in Seat names app/Seat.sharp. docs/architecture/spec-examples.md says how a fragment's types become files.

Verification

  • The same 185 examples pass. Both runs used the engine and mago built from master d31140d: Checked 201 examples, ignored 1, and 16 fail before and after. diff of the two passing lists prints nothing. Both lists are below.
  • The 16 failing examples fail on the same lines. The full reports differ in one problem of one failing example of section 31: line 8 of the generated app/Result.sharp, from the marker's setup or wrapper: Parse error encountered during parsing Expected one of Semicolon, CloseTag, found Public is gone. That parse error came from the next type in the shared file, which now has its own file. The example's own error, at docs/spec.md:4038, stays.
  • Self-test: sharp/bin/spec --self-test gives 21 of 21 self-test examples give the problems they expect. Master's runner fails the new file-per-type. It reports the error in app/Ticket.sharp, the one file both types share.
The 185 examples that pass before, on master's runner
access-on-get
any-checks
array-object-replacements
attribute-arguments
backed-enum
bare-name-patterns
behavior-class
bitwise
bool-conditions
braces
capture-and-change
capture-by-reference
change-observers
changed-copy-returned
class-header
class-law
class-members
class-value-equality
class-values
collection-literals
collection-read-methods
collection-write-methods
constructor-call-this
continue-in-condition
conversions
copy-on-write
dot-access
effectful-declarations
element-checks
emit-effect
emit-forms
empty-literals
enum-data-cases
enum-interfaces
enum-map-keys
enum-shapes
environment
environment-module
event-declarations
extension-hidden-member
extension-overloads
extension-static-choice
extension-unused-type-parameter
extensions
extern-declaration
extern-event
extern-event-effects
extern-functions
extern-implementations
field-and-property
file-header
for-of
foreign-class
foreign-constructor
function-fields
function-type-effects
function-types
generic-class-values
generic-extensions
generic-implementation
generic-statics
getter-writes
import-rename
impossible-patterns
index-changes
index-rules
inferred-locals
inferred-type-arguments
initial-value-storage
initial-values
initializer-shorthand
int-power
int-to-float
integer-division
interface-defaults
interface-members
is-and-as
is-not-scope
iterables
json-encoding
keyword-members
keyword-names
lambdas
lazy-property
leave-on-null
library-constants
library-functions
lifted-equality
listener-errors
listeners
literal-kinds
map-reads
map-string-keys
map-values
match-coverage
match-forms
method-declarations
method-values
missing-service
module-errors
module-law
module-services
named-arguments
named-constructors
namespace-and-import
naming-cases
narrowing
natural-remove
needless-null-checks
negative-tests
not-or-parentheses
null-defaults
nullable-map-values
nullable-members
nullable-unions
object-type
one-body-per-method
one-dispatcher
one-method-per-name
override-parameter-names
own-namespace-shadows
parameter-members
pattern-locals
pattern-scope
php-trait-defaults
plain-php-calls
plain-php-float-arguments
plain-php-overrides
plain-php-property-override
plain-php-traits
property-behaviors
property-collections
property-narrowing
property-patterns
property-references
psr-listeners
raw-generic-objects
readme-cart
readonly-by-definition
readonly-cannot-matter
readonly-fields
readonly-last-clause
readonly-overrides
readonly-positions
readonly-stays-readonly
readonly-values
required-constructors
scoped-binding
scoped-listener
sealed-interfaces
self-on-type-parameters
self-parameter
self-return
set-arguments
set-once
set-variable-arguments
spreads
standalone-event
standard-library-import
state-machine-law
static-initial-values
string-joining
struct-changes
struct-equality
struct-parse
struct-parse-object
sub-interface-defaults
super-call
super-calls
template-types
templates
test-module
this-members
tuples
type-tests
typed-locals
typed-loop-variables
typeof-type-parameters
typeof-types
typeof-values
unchecked-any
union-types
unreachable-statements
virtual-methods
yield
The 185 examples that pass after, on this PR's runner
access-on-get
any-checks
array-object-replacements
attribute-arguments
backed-enum
bare-name-patterns
behavior-class
bitwise
bool-conditions
braces
capture-and-change
capture-by-reference
change-observers
changed-copy-returned
class-header
class-law
class-members
class-value-equality
class-values
collection-literals
collection-read-methods
collection-write-methods
constructor-call-this
continue-in-condition
conversions
copy-on-write
dot-access
effectful-declarations
element-checks
emit-effect
emit-forms
empty-literals
enum-data-cases
enum-interfaces
enum-map-keys
enum-shapes
environment
environment-module
event-declarations
extension-hidden-member
extension-overloads
extension-static-choice
extension-unused-type-parameter
extensions
extern-declaration
extern-event
extern-event-effects
extern-functions
extern-implementations
field-and-property
file-header
for-of
foreign-class
foreign-constructor
function-fields
function-type-effects
function-types
generic-class-values
generic-extensions
generic-implementation
generic-statics
getter-writes
import-rename
impossible-patterns
index-changes
index-rules
inferred-locals
inferred-type-arguments
initial-value-storage
initial-values
initializer-shorthand
int-power
int-to-float
integer-division
interface-defaults
interface-members
is-and-as
is-not-scope
iterables
json-encoding
keyword-members
keyword-names
lambdas
lazy-property
leave-on-null
library-constants
library-functions
lifted-equality
listener-errors
listeners
literal-kinds
map-reads
map-string-keys
map-values
match-coverage
match-forms
method-declarations
method-values
missing-service
module-errors
module-law
module-services
named-arguments
named-constructors
namespace-and-import
naming-cases
narrowing
natural-remove
needless-null-checks
negative-tests
not-or-parentheses
null-defaults
nullable-map-values
nullable-members
nullable-unions
object-type
one-body-per-method
one-dispatcher
one-method-per-name
override-parameter-names
own-namespace-shadows
parameter-members
pattern-locals
pattern-scope
php-trait-defaults
plain-php-calls
plain-php-float-arguments
plain-php-overrides
plain-php-property-override
plain-php-traits
property-behaviors
property-collections
property-narrowing
property-patterns
property-references
psr-listeners
raw-generic-objects
readme-cart
readonly-by-definition
readonly-cannot-matter
readonly-fields
readonly-last-clause
readonly-overrides
readonly-positions
readonly-stays-readonly
readonly-values
required-constructors
scoped-binding
scoped-listener
sealed-interfaces
self-on-type-parameters
self-parameter
self-return
set-arguments
set-once
set-variable-arguments
spreads
standalone-event
standard-library-import
state-machine-law
static-initial-values
string-joining
struct-changes
struct-equality
struct-parse
struct-parse-object
sub-interface-defaults
super-call
super-calls
template-types
templates
test-module
this-members
tuples
type-tests
typed-locals
typed-loop-variables
typeof-type-parameters
typeof-types
typeof-values
unchecked-any
union-types
unreachable-statements
virtual-methods
yield

Section 27 says a .sharp file that declares a type declares one. The runner
wrote every type of a block, its setup and its example class into one file,
which the rule will refuse.

- build_files returns one file per type: split_types leads each with the
  file's namespace and import lines, and keeps what is no type, such as an
  extern line or a module block, with the first type. Each file keeps its own
  line map.
- A block whose first line is a path comment stays one file as written, so
  one-type-per-file still shows its error.
- Two files of one example at one path stop the run, in place of the rename
  to Name1.sharp, which hid two types of one name.

sharp/spec-self-test.md gains file-per-type, and docs/architecture/spec-examples.md
says how a fragment's types become files.
@heyJordanParker
heyJordanParker merged commit 849232f into master Oct 16, 2026
20 checks passed
heyJordanParker added a commit that referenced this pull request Oct 16, 2026
Master gained #144, CI's base images, and #147, sharp/bin/spec writing
each type of an example to a file of its own. SPEC_GUARD compares this
branch with master's tip, so it read master's sharp/bin/spec change as
this branch's.
heyJordanParker added a commit that referenced this pull request Oct 16, 2026
Master brings #142, #146, #147, #148 and #149. Each conflict keeps both
sides' behavior:

- A lambda's result type: master's `AnalysisArtifacts::lambda_result` is
  the one mechanism. `try` in a lambda reads it, and #143's
  `BlockContext::lambda_result_type` with its setters is gone.
- A `match`'s declared type reaching its PHP and its arms: master's code,
  with `Expression::Match` kept in `takes_declared_type`, without which
  the PHP `match` takes no declared type.
- `==` on a struct or a case: master's per-side type arguments for a
  generic struct, and a generic enum's case fields read the enum's type
  arguments from the side that gives them, through `with_type_arguments`.
- A call's declared type: master's `declared_return_type` goes through
  `as_declared_ancestor`, so `Result.Ok(…)` into a `Result<int, E>` fixes
  the case's type arguments for the arguments and for the result.
- The bridge: master's `generic_statics`, with `class_metadata` taking the
  class it describes, as a generic enum's case needs.
- Spec section 11 keeps master's inference bullets and #143's rule for a
  static call of a generic class.
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