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 ----
|
// ---- 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 {
|
export class FluentRelation {
|
||||||
private _assertion: FluentAssertion
|
private _assertion: FluentAssertion
|
||||||
private _relation: string
|
private _relation: string
|
||||||
private _referenceSelector: string
|
private _referenceSelector: string
|
||||||
private _options: RelationOptions
|
private _options: RelationOptions
|
||||||
private _compoundParts: Array<{ relation: string; referenceSelector: string; options: RelationOptions }> = []
|
private _compoundParts: CompoundPart[] = []
|
||||||
private _compoundOperator?: 'and' | 'or'
|
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._assertion = assertion
|
||||||
this._relation = relation
|
this._relation = relation
|
||||||
this._referenceSelector = referenceSelector as string
|
this._referenceSelector = referenceSelector as string
|
||||||
this._options = options
|
this._options = options
|
||||||
|
if (compoundState) {
|
||||||
|
this._compoundParts = compoundState.parts
|
||||||
|
this._compoundOperator = compoundState.operator
|
||||||
|
this._originalFirstRelation = compoundState.firstRelation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get assertion(): FluentAssertion {
|
get assertion(): FluentAssertion {
|
||||||
@@ -675,21 +701,29 @@ export class FluentRelation {
|
|||||||
return this._options
|
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')
|
// Chain: .and.above('.c') => leftOf('.b') AND above('.c')
|
||||||
get and(): FluentCompoundBuilder {
|
get and(): FluentCompoundBuilder {
|
||||||
// If this relation is already a compound result, continue accumulating
|
if (this.isCompound) {
|
||||||
// instead of starting a fresh builder. This enables triple+ chaining:
|
const originalFirst = this._originalFirstRelation
|
||||||
// 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
|
|
||||||
const firstRelation = originalFirst ?? this
|
const firstRelation = originalFirst ?? this
|
||||||
const builder = new FluentCompoundBuilder(firstRelation, 'and')
|
const builder = new FluentCompoundBuilder(firstRelation, 'and')
|
||||||
// Pre-populate with all existing compound parts (skip [0] which is the
|
const existingParts = this._compoundParts
|
||||||
// first relation — the builder constructor already captures it).
|
if (existingParts.length > 1) {
|
||||||
const existingParts = (this as any)._compoundParts as Array<{ relation: string; referenceSelector: string; options: RelationOptions }>
|
|
||||||
if (existingParts && existingParts.length > 1) {
|
|
||||||
for (let i = 1; i < existingParts.length; i++) {
|
for (let i = 1; i < existingParts.length; i++) {
|
||||||
builder.getParts().push(existingParts[i])
|
builder.getParts().push(existingParts[i])
|
||||||
}
|
}
|
||||||
@@ -701,13 +735,12 @@ export class FluentRelation {
|
|||||||
|
|
||||||
// Chain: .or.below('.c') => leftOf('.b') OR below('.c')
|
// Chain: .or.below('.c') => leftOf('.b') OR below('.c')
|
||||||
get or(): FluentCompoundBuilder {
|
get or(): FluentCompoundBuilder {
|
||||||
// Same compound-continuation logic as .and
|
if (this.isCompound) {
|
||||||
if ((this as any)._isCompound) {
|
const originalFirst = this._originalFirstRelation
|
||||||
const originalFirst = (this as any)._originalFirstRelation as FluentRelation | undefined
|
|
||||||
const firstRelation = originalFirst ?? this
|
const firstRelation = originalFirst ?? this
|
||||||
const builder = new FluentCompoundBuilder(firstRelation, 'or')
|
const builder = new FluentCompoundBuilder(firstRelation, 'or')
|
||||||
const existingParts = (this as any)._compoundParts as Array<{ relation: string; referenceSelector: string; options: RelationOptions }>
|
const existingParts = this._compoundParts
|
||||||
if (existingParts && existingParts.length > 1) {
|
if (existingParts.length > 1) {
|
||||||
for (let i = 1; i < existingParts.length; i++) {
|
for (let i = 1; i < existingParts.length; i++) {
|
||||||
builder.getParts().push(existingParts[i])
|
builder.getParts().push(existingParts[i])
|
||||||
}
|
}
|
||||||
@@ -761,7 +794,7 @@ export class FluentRelation {
|
|||||||
export class FluentCompoundBuilder {
|
export class FluentCompoundBuilder {
|
||||||
private _firstRelation: FluentRelation
|
private _firstRelation: FluentRelation
|
||||||
private _operator: 'and' | 'or'
|
private _operator: 'and' | 'or'
|
||||||
private _parts: Array<{ relation: string; referenceSelector: string; options: RelationOptions }> = []
|
private _parts: CompoundPart[] = []
|
||||||
|
|
||||||
constructor(firstRelation: FluentRelation, operator: 'and' | 'or') {
|
constructor(firstRelation: FluentRelation, operator: 'and' | 'or') {
|
||||||
this._firstRelation = firstRelation
|
this._firstRelation = firstRelation
|
||||||
@@ -769,7 +802,7 @@ export class FluentCompoundBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Package-private: allow compound continuation to pre-populate parts. */
|
/** Package-private: allow compound continuation to pre-populate parts. */
|
||||||
getParts(): Array<{ relation: string; referenceSelector: string; options: RelationOptions }> {
|
getParts(): CompoundPart[] {
|
||||||
return this._parts
|
return this._parts
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,17 +849,15 @@ export class FluentCompoundBuilder {
|
|||||||
|
|
||||||
private _addPart(relation: string, referenceSelector: string, options: RelationOptions): FluentRelation {
|
private _addPart(relation: string, referenceSelector: string, options: RelationOptions): FluentRelation {
|
||||||
this._parts.push({ relation, referenceSelector, options })
|
this._parts.push({ relation, referenceSelector, options })
|
||||||
const compound = new FluentRelation(this._firstRelation.assertion, relation, referenceSelector, options)
|
const compoundState: CompoundState = {
|
||||||
// Tag the compound relation for extraction and continuation
|
operator: this._operator,
|
||||||
;(compound as any)._isCompound = true
|
parts: [
|
||||||
;(compound as any)._compoundOperator = this._operator
|
|
||||||
;(compound as any)._compoundParts = [
|
|
||||||
{ relation: this._firstRelation.relation, referenceSelector: this._firstRelation.referenceSelector, options: this._firstRelation.options },
|
{ relation: this._firstRelation.relation, referenceSelector: this._firstRelation.referenceSelector, options: this._firstRelation.options },
|
||||||
...this._parts,
|
...this._parts,
|
||||||
]
|
],
|
||||||
// Store the original first relation so .and/.or continuation works correctly
|
firstRelation: this._firstRelation,
|
||||||
;(compound as any)._originalFirstRelation = this._firstRelation
|
}
|
||||||
return compound
|
return new FluentRelation(this._firstRelation.assertion, relation, referenceSelector, options, compoundState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1318,8 +1318,9 @@ export function getSelectorsFromAssertion(assertion: FluentRelation | FluentAsse
|
|||||||
selectors.add(rel.referenceSelector)
|
selectors.add(rel.referenceSelector)
|
||||||
}
|
}
|
||||||
// Extract selectors from compound relation parts (.and / .or chaining)
|
// Extract selectors from compound relation parts (.and / .or chaining)
|
||||||
if (Array.isArray(rel._compoundParts)) {
|
const compoundRel = assertion as FluentRelation
|
||||||
for (const part of rel._compoundParts) {
|
if (compoundRel.isCompound) {
|
||||||
|
for (const part of compoundRel.compoundParts) {
|
||||||
if (part.referenceSelector) {
|
if (part.referenceSelector) {
|
||||||
selectors.add(part.referenceSelector)
|
selectors.add(part.referenceSelector)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,8 +228,8 @@ function compileRelation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle compound relations (.and / .or chaining)
|
// Handle compound relations (.and / .or chaining)
|
||||||
const compoundParts = (relation as any)._compoundParts as Array<{ relation: string; referenceSelector: string; options: Record<string, unknown> }> | undefined
|
const compoundParts = relation.compoundParts
|
||||||
const compoundOperator = (relation as any)._compoundOperator as 'and' | 'or' | undefined
|
const compoundOperator = relation.compoundOperator
|
||||||
|
|
||||||
if (compoundParts && compoundParts.length > 1 && compoundOperator) {
|
if (compoundParts && compoundParts.length > 1 && compoundOperator) {
|
||||||
const assertionSpace = getAssertionSpace()
|
const assertionSpace = getAssertionSpace()
|
||||||
@@ -415,7 +415,7 @@ function compileSingleAssertion(
|
|||||||
const refSel = rel.referenceSelector
|
const refSel = rel.referenceSelector
|
||||||
const quantifier = rel.assertion.getQuantifier()
|
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 subjectVar = makeVar('$subject')
|
||||||
const refVar = makeVar('$reference')
|
const refVar = makeVar('$reference')
|
||||||
|
|||||||
Reference in New Issue
Block a user