Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ The framework must run on Lucee 5/6/7, Adobe CF 2018/2021/2023/2025, and BoxLang
8. **`Left(str, 0)` crashes Lucee 7.** Guard: `len > 0 ? Left(str, len) : ""`.
9. **`toBeInstanceOf("component")` fails on BoxLang** — returns the FQN, not the literal `"component"`. Use `toBeWheelsModel()` for finder results.
10. **Adobe CF 2023 and 2025 reject the `arguments` scope as `attributeCollection` on *any* built-in CFML tag.** Affects every `cfheader` / `cfcache` / `cfcontent` / `cfmail` / `cfdirectory` / `cffile` / `cflocation` / `cfhtmlhead` / `cfimage` / `cfdbinfo` / `cfinvoke` / `cfwddx` / `cfzip` wrapper. Covers both the string-interpolated (`attributeCollection = "#arguments#"`) and direct-struct (`attributeCollection = arguments`) forms. Adobe 2023/2025 throw — `cfheader`'s message is `"Failed to add HTML header"`; other tags surface their own — and `$header()` is catastrophic because it runs on every request. Copy to a plain struct first: `local.args = {}; for (local.key in arguments) { local.args[local.key] = arguments[local.key]; }`. Lucee 6/7, BoxLang, and Adobe 2018/2021 accept both forms; Adobe 2023/2025 require the plain struct. The 13 sites in `vendor/wheels/Global.cfc` were patched uniformly in [#2750](https://github.com/wheels-dev/wheels/pull/2750).
11. **`local.X = ...` inside `catch` doesn't persist on BoxLang.** Catch body runs under a nested `local` that gets discarded on exit, so `expect(local.X)` after the catch reads the un-touched outer value. Use a struct field: `var state = {flag = false}; ... state.flag = true;`. Bare `var bareName` + unscoped `bareName = true` also works but the struct form mirrors `TenantResolverSpec` and is the prior-art pattern.
11. **Anything written through `local.` inside `catch` doesn't persist on BoxLang.** Catch body runs under a nested `local` that gets discarded on exit, so `expect(local.X)` after the catch reads the un-touched outer value. Use a struct field: `var state = {flag = false}; ... state.flag = true;`. Bare `var bareName` + unscoped `bareName = true` also works but the struct form mirrors `TenantResolverSpec` and is the prior-art pattern.

**The struct form only works if you access it WITHOUT the `local.` prefix.** `local.state.flag = true` inside a catch fails exactly like a scalar `local.X = ...` — the nested `local` shadows `local.state`, so the write lands on a discarded copy rather than mutating the outer struct. The prefix is what breaks it, not the assignment shape:
```cfm
var state = {type = ""}; // RIGHT
try { ... } catch (any e) { state.type = e.type; }
local.state = {type = ""}; // WRONG — silently empty after the catch
try { ... } catch (any e) { local.state.type = e.type; }
```
This matters because `local.`-scoping spec variables is the house style everywhere else, so "tidying" a catch-using spec to match is an easy and invisible way to break it. Doing exactly that to `JobClassRoundTripSpec` cost two BoxLang failures on every database (`Expected [Wheels.JobClassNotFound] but received []`) — green on Lucee, caught only by the compat matrix.
12. **`for (local.i = ...)` inside `finally` miscompiles on Lucee 7.** Lucee 7.0.1+100 throws `variable [local] doesn't exist` at runtime when a `for` loop declares or iterates `local`-/`var`-scoped variables inside a `finally` block (one probe shape even produced a JVM `Expecting a stackmap frame` verifier error). Bare assignments and function calls in `finally` are fine; loops are not. Hoist the loop into a `public` `$`-prefixed helper and call it from `finally` — reference: `$restoreEmailViewVariables()` in `vendor/wheels/controller/miscellaneous.cfc` ([#2922](https://github.com/wheels-dev/wheels/pull/2922)).
13. **Bare tag-in-script statements without parentheses (e.g. `cfabort;`) are Lucee-only.** Adobe CF compiles the bare token as a reference to an undefined VARIABLE and throws `Variable CFABORT is undefined` at runtime (every Adobe engine, not just one release). Use the script keyword (`abort;`) or the parenthesized call form (`cfheader(...)`-style) instead. The `enablePublicComponent=false` 404 branch in `vendor/wheels/Dispatch.cfc` shipped a bare `cfabort;`, which turned `GET /` on every stock Adobe install in `testing`/`production` into an HTTP 500 ([#3029](https://github.com/wheels-dev/wheels/issues/3029)). Structural guard: `vendor/wheels/tests/specs/security/BareCfabortGuardSpec.cfc` fails the suite if any bare script-context `cfabort` statement reappears under `vendor/wheels/**/*.cfc` (tag-context `<cfabort>` in `.cfm`/tag-based CFCs stays legal).
14. **Adobe 2025's JVM rejects member calls on JDK-internal classes (JPMS).** Calling any member on an object whose runtime class lives in an unexported package (`com.sun.*`, `jdk.internal.*`) — e.g. the `com.sun.crypto.provider.PBKDF2KeyImpl` returned by `SecretKeyFactory.generateSecret()` — throws `java.lang.reflect.InaccessibleObjectException` on Adobe 2025 (its reflection layer bulk-`setAccessible`s the concrete class's methods; Lucee, BoxLang, and Adobe ≤2023 tolerate the same call, so **local Adobe 2023 green does NOT cover this**). Route the call through the exported interface's `Method` object instead: `CreateObject("java","java.lang.Class").forName("javax.crypto.SecretKey").getMethod("getEncoded", JavaCast("null","")).invoke(keyObj, JavaCast("null",""))` — `getMethod`/`invoke` treat the null varargs as empty. Hit by `PasswordHasher.$deriveKey()` ([#3300](https://github.com/wheels-dev/wheels/issues/3300)); watch for it with any Java factory API that returns internal implementation types.
Expand Down
52 changes: 52 additions & 0 deletions public/testbox/system/stubs/139184C0543CCDB338AEFB643110CB89.cfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<cfscript>
variables[ "closeSSEStream" ] = variables[ "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ];
this[ "closeSSEStream" ] = variables[ "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" ];

// Clean up
structDelete( variables, "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" );
structDelete( this, "tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89" );
public void function tmp_closeSSEStream_139184C0543CCDB338AEFB643110CB89(

) output=true {

var results = this._mockResults;
var resultsKey = "closeSSEStream";
var resultsCounter = 0;
var internalCounter = 0;
var resultsLen = 0;
var callbackLen = 0;
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
var fCallBack = "";

// If Method & argument Hash Results, switch the results struct
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
// Check if it is a callback
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
fCallBack = this._mockArgResults[ argsHashKey ].target;
} else {
// switch context and key
results = this._mockArgResults;
resultsKey = argsHashKey;
}
}

// Get the statemachine counter
if (isSimpleValue( fCallBack) ) {
resultsLen = arrayLen( results[ resultsKey ] );
}

// Get the callback counter, if it exists
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
}

// Log the Method Call
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;

// Get the CallCounter Reference
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
arrayAppend( this._mockCallLoggers["closeSSEStream"], arguments );
}
</cfscript>
52 changes: 52 additions & 0 deletions public/testbox/system/stubs/16CA394252B074478BEBD2B3A9C8EA82.cfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<cfscript>
variables[ "sendSSEComment" ] = variables[ "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ];
this[ "sendSSEComment" ] = variables[ "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" ];

// Clean up
structDelete( variables, "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" );
structDelete( this, "tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82" );
public void function tmp_sendSSEComment_16CA394252B074478BEBD2B3A9C8EA82(

) output=true {

var results = this._mockResults;
var resultsKey = "sendSSEComment";
var resultsCounter = 0;
var internalCounter = 0;
var resultsLen = 0;
var callbackLen = 0;
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
var fCallBack = "";

// If Method & argument Hash Results, switch the results struct
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
// Check if it is a callback
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
fCallBack = this._mockArgResults[ argsHashKey ].target;
} else {
// switch context and key
results = this._mockArgResults;
resultsKey = argsHashKey;
}
}

// Get the statemachine counter
if (isSimpleValue( fCallBack) ) {
resultsLen = arrayLen( results[ resultsKey ] );
}

// Get the callback counter, if it exists
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
}

// Log the Method Call
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;

// Get the CallCounter Reference
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
arrayAppend( this._mockCallLoggers["sendSSEComment"], arguments );
}
</cfscript>
70 changes: 70 additions & 0 deletions public/testbox/system/stubs/30942F4D0BCB6139072EF27C66218715.cfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<cfscript>
variables[ "checkError" ] = variables[ "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ];
this[ "checkError" ] = variables[ "tmp_checkError_30942F4D0BCB6139072EF27C66218715" ];

// Clean up
structDelete( variables, "tmp_checkError_30942F4D0BCB6139072EF27C66218715" );
structDelete( this, "tmp_checkError_30942F4D0BCB6139072EF27C66218715" );
public any function tmp_checkError_30942F4D0BCB6139072EF27C66218715(

) output=true {

var results = this._mockResults;
var resultsKey = "checkError";
var resultsCounter = 0;
var internalCounter = 0;
var resultsLen = 0;
var callbackLen = 0;
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
var fCallBack = "";

// If Method & argument Hash Results, switch the results struct
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
// Check if it is a callback
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
fCallBack = this._mockArgResults[ argsHashKey ].target;
} else {
// switch context and key
results = this._mockArgResults;
resultsKey = argsHashKey;
}
}

// Get the statemachine counter
if (isSimpleValue( fCallBack) ) {
resultsLen = arrayLen( results[ resultsKey ] );
}

// Get the callback counter, if it exists
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
}

// Log the Method Call
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;

// Get the CallCounter Reference
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
arrayAppend( this._mockCallLoggers["checkError"], arguments );

if (resultsLen neq 0) {
if (internalCounter gt resultsLen) {
resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) );
return results[ resultsKey ][ resultsCounter ];
} else {
return results[ resultsKey ][ internalCounter ];
}
}

if ( callbackLen neq 0 ) {
fCallBack = this._mockCallbacks[ resultsKey ].first();
return fCallBack( argumentCollection : arguments );
}

if ( not isSimpleValue( fCallBack ) ){
return fCallBack( argumentCollection : arguments );
}
}
</cfscript>
52 changes: 52 additions & 0 deletions public/testbox/system/stubs/754BEF48E30B63BC11E518FA73C07785.cfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<cfscript>
variables[ "sendSSEEvent" ] = variables[ "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ];
this[ "sendSSEEvent" ] = variables[ "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" ];

// Clean up
structDelete( variables, "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" );
structDelete( this, "tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785" );
public void function tmp_sendSSEEvent_754BEF48E30B63BC11E518FA73C07785(

) output=true {

var results = this._mockResults;
var resultsKey = "sendSSEEvent";
var resultsCounter = 0;
var internalCounter = 0;
var resultsLen = 0;
var callbackLen = 0;
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
var fCallBack = "";

// If Method & argument Hash Results, switch the results struct
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
// Check if it is a callback
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
fCallBack = this._mockArgResults[ argsHashKey ].target;
} else {
// switch context and key
results = this._mockArgResults;
resultsKey = argsHashKey;
}
}

// Get the statemachine counter
if (isSimpleValue( fCallBack) ) {
resultsLen = arrayLen( results[ resultsKey ] );
}

// Get the callback counter, if it exists
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
}

// Log the Method Call
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;

// Get the CallCounter Reference
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
arrayAppend( this._mockCallLoggers["sendSSEEvent"], arguments );
}
</cfscript>
70 changes: 70 additions & 0 deletions public/testbox/system/stubs/B7681C49C5125395612F24D97559A4C2.cfm
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<cfscript>
variables[ "initSSEStream" ] = variables[ "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ];
this[ "initSSEStream" ] = variables[ "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" ];

// Clean up
structDelete( variables, "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" );
structDelete( this, "tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2" );
public any function tmp_initSSEStream_B7681C49C5125395612F24D97559A4C2(

) output=true {

var results = this._mockResults;
var resultsKey = "initSSEStream";
var resultsCounter = 0;
var internalCounter = 0;
var resultsLen = 0;
var callbackLen = 0;
var argsHashKey = resultsKey & "|" & this.mockBox.normalizeArguments( arguments );
var fCallBack = "";

// If Method & argument Hash Results, switch the results struct
if (structKeyExists( this._mockArgResults, argsHashKey) ) {
// Check if it is a callback
if (isStruct( this._mockArgResults[ argsHashKey ]) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "type" ) &&
structKeyExists( this._mockArgResults[ argsHashKey ], "target" ) ) {
fCallBack = this._mockArgResults[ argsHashKey ].target;
} else {
// switch context and key
results = this._mockArgResults;
resultsKey = argsHashKey;
}
}

// Get the statemachine counter
if (isSimpleValue( fCallBack) ) {
resultsLen = arrayLen( results[ resultsKey ] );
}

// Get the callback counter, if it exists
if (structKeyExists( this._mockCallbacks, resultsKey) ) {
callbackLen = arrayLen( this._mockCallbacks[ resultsKey ] );
}

// Log the Method Call
this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] = this._mockMethodCallCounters[ listFirst( resultsKey, "|" ) ] + 1;

// Get the CallCounter Reference
internalCounter = this._mockMethodCallCounters[listFirst(resultsKey,"|")];
arrayAppend( this._mockCallLoggers["initSSEStream"], arguments );

if (resultsLen neq 0) {
if (internalCounter gt resultsLen) {
resultsCounter = internalCounter - ( resultsLen * fix( ( internalCounter - 1 ) / resultsLen ) );
return results[ resultsKey ][ resultsCounter ];
} else {
return results[ resultsKey ][ internalCounter ];
}
}

if ( callbackLen neq 0 ) {
fCallBack = this._mockCallbacks[ resultsKey ].first();
return fCallBack( argumentCollection : arguments );
}

if ( not isSimpleValue( fCallBack ) ){
return fCallBack( argumentCollection : arguments );
}
}
</cfscript>
Loading
Loading