refactor: replace smuggled compound properties with typed FluentRelation API
fluent.ts: Add public getters isCompound, compoundOperator, compoundParts to FluentRelation. Add optional CompoundState to constructor. Add CompoundPart/CompoundState interfaces. Replace 4 (this as any)._xxx property smuggles in FluentCompoundBuilder._addPart() with properly typed constructor initialization via CompoundState. fol-compiler.ts: Replace 3 (relation as any)._compoundParts / ._compoundOperator / ._isCompound duck-type reads with direct relation.compoundParts / relation.compoundOperator / relation.isCompound. extraction.ts: Replace (rel as any)._compoundParts read in getSelectorsFromAssertion with typed compoundRel.isCompound / compoundRel.compoundParts access. Eliminates 7 (as any) casts across 3 production files. Zero remaining _compoundParts/_compoundOperator/_isCompound/_originalFirstRelation smuggled references in any production source file. 598 SDK + 3 conformance + 57 E2E = 658 tests pass.
This commit is contained in:
@@ -644,19 +644,45 @@ export class HaveProxy {
|
||||
|
||||
// ---- Fluent Relation Terminal ----
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compound relation types (replaces smuggled _compoundParts / _compoundOperator)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface CompoundPart {
|
||||
relation: string
|
||||
referenceSelector: string
|
||||
options: RelationOptions
|
||||
}
|
||||
|
||||
export interface CompoundState {
|
||||
operator: 'and' | 'or'
|
||||
parts: CompoundPart[]
|
||||
firstRelation: FluentRelation
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fluent Relation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class FluentRelation {
|
||||
private _assertion: FluentAssertion
|
||||
private _relation: string
|
||||
private _referenceSelector: string
|
||||
private _options: RelationOptions
|
||||
private _compoundParts: Array<{ relation: string; referenceSelector: string; options: RelationOptions }> = []
|
||||
private _compoundParts: CompoundPart[] = []
|
||||
private _compoundOperator?: 'and' | 'or'
|
||||
private _originalFirstRelation?: FluentRelation
|
||||
|
||||
constructor(assertion: FluentAssertion, relation: string, referenceSelector: string | SubjectReference, options: RelationOptions) {
|
||||
constructor(assertion: FluentAssertion, relation: string, referenceSelector: string | SubjectReference, options: RelationOptions, compoundState?: CompoundState) {
|
||||
this._assertion = assertion
|
||||
this._relation = relation
|
||||
this._referenceSelector = referenceSelector as string
|
||||
this._options = options
|
||||
if (compoundState) {
|
||||
this._compoundParts = compoundState.parts
|
||||
this._compoundOperator = compoundState.operator
|
||||
this._originalFirstRelation = compoundState.firstRelation
|
||||
}
|
||||
}
|
||||
|
||||
get assertion(): FluentAssertion {
|
||||
@@ -675,21 +701,29 @@ export class FluentRelation {
|
||||
return this._options
|
||||
}
|
||||
|
||||
/** Whether this relation represents a compound (.and / .or) chain. */
|
||||
get isCompound(): boolean {
|
||||
return this._compoundParts.length > 1 && this._compoundOperator !== undefined
|
||||
}
|
||||
|
||||
/** Logical operator for compound chains (undefined for simple relations). */
|
||||
get compoundOperator(): 'and' | 'or' | undefined {
|
||||
return this._compoundOperator
|
||||
}
|
||||
|
||||
/** Sub-relations that form the compound chain (empty for simple relations). */
|
||||
get compoundParts(): ReadonlyArray<CompoundPart> {
|
||||
return this._compoundParts
|
||||
}
|
||||
|
||||
// Chain: .and.above('.c') => leftOf('.b') AND above('.c')
|
||||
get and(): FluentCompoundBuilder {
|
||||
// If this relation is already a compound result, continue accumulating
|
||||
// instead of starting a fresh builder. This enables triple+ chaining:
|
||||
// expect('.a').to.be.leftOf('.b').and.above('.c').and.inside('.d')
|
||||
if ((this as any)._isCompound) {
|
||||
// Use the original first relation, not this compound relation (whose
|
||||
// _relation is the last-added relation, e.g. 'above').
|
||||
const originalFirst = (this as any)._originalFirstRelation as FluentRelation | undefined
|
||||
if (this.isCompound) {
|
||||
const originalFirst = this._originalFirstRelation
|
||||
const firstRelation = originalFirst ?? this
|
||||
const builder = new FluentCompoundBuilder(firstRelation, 'and')
|
||||
// Pre-populate with all existing compound parts (skip [0] which is the
|
||||
// first relation — the builder constructor already captures it).
|
||||
const existingParts = (this as any)._compoundParts as Array<{ relation: string; referenceSelector: string; options: RelationOptions }>
|
||||
if (existingParts && existingParts.length > 1) {
|
||||
const existingParts = this._compoundParts
|
||||
if (existingParts.length > 1) {
|
||||
for (let i = 1; i < existingParts.length; i++) {
|
||||
builder.getParts().push(existingParts[i])
|
||||
}
|
||||
@@ -701,13 +735,12 @@ export class FluentRelation {
|
||||
|
||||
// Chain: .or.below('.c') => leftOf('.b') OR below('.c')
|
||||
get or(): FluentCompoundBuilder {
|
||||
// Same compound-continuation logic as .and
|
||||
if ((this as any)._isCompound) {
|
||||
const originalFirst = (this as any)._originalFirstRelation as FluentRelation | undefined
|
||||
if (this.isCompound) {
|
||||
const originalFirst = this._originalFirstRelation
|
||||
const firstRelation = originalFirst ?? this
|
||||
const builder = new FluentCompoundBuilder(firstRelation, 'or')
|
||||
const existingParts = (this as any)._compoundParts as Array<{ relation: string; referenceSelector: string; options: RelationOptions }>
|
||||
if (existingParts && existingParts.length > 1) {
|
||||
const existingParts = this._compoundParts
|
||||
if (existingParts.length > 1) {
|
||||
for (let i = 1; i < existingParts.length; i++) {
|
||||
builder.getParts().push(existingParts[i])
|
||||
}
|
||||
@@ -761,7 +794,7 @@ export class FluentRelation {
|
||||
export class FluentCompoundBuilder {
|
||||
private _firstRelation: FluentRelation
|
||||
private _operator: 'and' | 'or'
|
||||
private _parts: Array<{ relation: string; referenceSelector: string; options: RelationOptions }> = []
|
||||
private _parts: CompoundPart[] = []
|
||||
|
||||
constructor(firstRelation: FluentRelation, operator: 'and' | 'or') {
|
||||
this._firstRelation = firstRelation
|
||||
@@ -769,7 +802,7 @@ export class FluentCompoundBuilder {
|
||||
}
|
||||
|
||||
/** Package-private: allow compound continuation to pre-populate parts. */
|
||||
getParts(): Array<{ relation: string; referenceSelector: string; options: RelationOptions }> {
|
||||
getParts(): CompoundPart[] {
|
||||
return this._parts
|
||||
}
|
||||
|
||||
@@ -816,17 +849,15 @@ export class FluentCompoundBuilder {
|
||||
|
||||
private _addPart(relation: string, referenceSelector: string, options: RelationOptions): FluentRelation {
|
||||
this._parts.push({ relation, referenceSelector, options })
|
||||
const compound = new FluentRelation(this._firstRelation.assertion, relation, referenceSelector, options)
|
||||
// Tag the compound relation for extraction and continuation
|
||||
;(compound as any)._isCompound = true
|
||||
;(compound as any)._compoundOperator = this._operator
|
||||
;(compound as any)._compoundParts = [
|
||||
const compoundState: CompoundState = {
|
||||
operator: this._operator,
|
||||
parts: [
|
||||
{ relation: this._firstRelation.relation, referenceSelector: this._firstRelation.referenceSelector, options: this._firstRelation.options },
|
||||
...this._parts,
|
||||
]
|
||||
// Store the original first relation so .and/.or continuation works correctly
|
||||
;(compound as any)._originalFirstRelation = this._firstRelation
|
||||
return compound
|
||||
],
|
||||
firstRelation: this._firstRelation,
|
||||
}
|
||||
return new FluentRelation(this._firstRelation.assertion, relation, referenceSelector, options, compoundState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1318,8 +1318,9 @@ export function getSelectorsFromAssertion(assertion: FluentRelation | FluentAsse
|
||||
selectors.add(rel.referenceSelector)
|
||||
}
|
||||
// Extract selectors from compound relation parts (.and / .or chaining)
|
||||
if (Array.isArray(rel._compoundParts)) {
|
||||
for (const part of rel._compoundParts) {
|
||||
const compoundRel = assertion as FluentRelation
|
||||
if (compoundRel.isCompound) {
|
||||
for (const part of compoundRel.compoundParts) {
|
||||
if (part.referenceSelector) {
|
||||
selectors.add(part.referenceSelector)
|
||||
}
|
||||
|
||||
@@ -228,8 +228,8 @@ function compileRelation(
|
||||
}
|
||||
|
||||
// Handle compound relations (.and / .or chaining)
|
||||
const compoundParts = (relation as any)._compoundParts as Array<{ relation: string; referenceSelector: string; options: Record<string, unknown> }> | undefined
|
||||
const compoundOperator = (relation as any)._compoundOperator as 'and' | 'or' | undefined
|
||||
const compoundParts = relation.compoundParts
|
||||
const compoundOperator = relation.compoundOperator
|
||||
|
||||
if (compoundParts && compoundParts.length > 1 && compoundOperator) {
|
||||
const assertionSpace = getAssertionSpace()
|
||||
@@ -415,7 +415,7 @@ function compileSingleAssertion(
|
||||
const refSel = rel.referenceSelector
|
||||
const quantifier = rel.assertion.getQuantifier()
|
||||
|
||||
const isCompound = !!(rel as any)._compoundParts && (rel as any)._compoundParts.length > 1
|
||||
const isCompound = rel.isCompound
|
||||
|
||||
const subjectVar = makeVar('$subject')
|
||||
const refVar = makeVar('$reference')
|
||||
|
||||
Reference in New Issue
Block a user