Skip to content

Commit d258a19

Browse files
authored
fix: sweep accumulated reviewer nits from the 2026-06 campaign (#2992)
* fix: sweep accumulated reviewer nits from the 2026-06 campaign Behavior fixes: LCase word-form operators in $evaluateLogicalExpression (uppercase EQ threw on Adobe CF); /wheels/cli gate reads the reload password from the form scope only (query-string password satisfied the gate while landing in access logs); dbRollback counts applied migrations by tracked status, not the version<=current heuristic (shared-dev-DB skew); NPE guard for null getErrorStream() in the three CLI HTTP bridge helpers; migrator column cache keys verbatim (no case folding); $getForeignKeys throws on missing adapter instead of emitting unquoted SQL; real \d escapes in $convertToString's dead ISO branch. Plus stale-docblock updates (#2903 references, renderWith/ onlyProvides enforcement notes, debug-panel guide note, .ai finally- loop note, migrator CLAUDE.md cache docs) and spec backfills (waitForText timeout, $get without request.wheels, typed-column outlier defaults, conditional loadRoutesSpec restore, uppercase-EQ conditions, dbDrop/dbRestore stub note). Fixes #2977 Signed-off-by: Peter Amiri <peter@alurium.com> * chore(docs): move changelog entry to changelog.d fragment Eliminates the [Unreleased]-anchor merge conflicts across campaign PRs; fragments are assembled into CHANGELOG.md at release promotion. Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: Peter Amiri <peter@alurium.com>
1 parent 1c9915e commit d258a19

19 files changed

Lines changed: 211 additions & 19 deletions

File tree

‎.ai/wheels/cross-engine-compatibility.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ createDynamicProxy(consumer, ["java.util.function.Consumer"]);
172172

173173
### `for` Loops Inside `finally` Blocks Miscompile on Lucee 7
174174

175-
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. Isolated with minimal probes: bare assignments and function calls inside `finally` compile and run fine; loops do not. One probe shape even produced a JVM `Expecting a stackmap frame` bytecode-verifier error, pointing at a codegen bug in Lucee's `finally`-block compilation.
175+
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. Both loop forms are affected — `for (init; cond; step)` and `for (item in collection)`. Isolated with minimal probes: bare assignments and function calls inside `finally` compile and run fine; loops do not. One probe shape even produced a JVM `Expecting a stackmap frame` bytecode-verifier error, pointing at a codegen bug in Lucee's `finally`-block compilation.
176176

177177
```cfm
178178
// WRONG — crashes at runtime on Lucee 7
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Reviewer-nit sweep from the 2026-06 remediation campaign (#2977). Behavior fixes: conditional validations with uppercase word-form operators (`condition="1 EQ 0"`) no longer throw on Adobe CF (`$evaluateLogicalExpression` now lowercases the operator); the `/wheels/cli` mutation gate reads the reload password from the form scope ONLY, so a `?password=...` query string can no longer satisfy the gate while logging the password in access logs; `dbRollback` over the `/wheels/cli` bridge counts applied migrations by tracked `status` instead of the `version <= current` heuristic, so peer-applied versions on a shared dev database no longer skew `steps=N`; the CLI's three HTTP bridge helpers guard against `getErrorStream()` returning Java null on bodiless 4xx/5xx responses (was an NPE surfacing as a useless "null" error); the migrator's per-request column cache keys on the verbatim table name (case-folding let `Authors`/`authors` share a slot on case-sensitive databases); `$getForeignKeys()` throws `Wheels.Migrator.MissingAdapter` instead of silently interpolating an unquoted table name when the adapter is missing; and the dead ISO-date fallback branch in `$convertToString` uses real `\d` regex escapes. Plus assorted stale-docblock/comment updates (#2903 references, `renderWith`/`onlyProvides` enforcement notes, debug-panel guide note) and spec backfills (`waitForText` timeout surface, `$get()` without `request.wheels`, typed-column outlier defaults, conditional spec-state restore) (#2977)

‎cli/lucli/Module.cfc‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6417,6 +6417,12 @@ component extends="modules.BaseModule" {
64176417

64186418
var responseCode = conn.getResponseCode();
64196419
var inputStream = responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream();
6420+
// getErrorStream() returns Java null on a bodiless 4xx/5xx response;
6421+
// Scanner.init(null) NPEs on Lucee and surfaces as a useless "null"
6422+
// error message (#2947 review, #2977). No body — return empty.
6423+
if (isNull(inputStream)) {
6424+
return "";
6425+
}
64206426
var scanner = createObject("java", "java.util.Scanner").init(inputStream, "UTF-8");
64216427
var response = "";
64226428
while (scanner.hasNextLine()) {
@@ -6450,6 +6456,12 @@ component extends="modules.BaseModule" {
64506456

64516457
var responseCode = conn.getResponseCode();
64526458
var inputStream = responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream();
6459+
// getErrorStream() returns Java null on a bodiless 4xx/5xx response;
6460+
// Scanner.init(null) NPEs on Lucee and surfaces as a useless "null"
6461+
// error message (#2947 review, #2977). No body — return empty.
6462+
if (isNull(inputStream)) {
6463+
return "";
6464+
}
64536465
var scanner = createObject("java", "java.util.Scanner").init(inputStream, "UTF-8");
64546466
var response = "";
64556467
while (scanner.hasNextLine()) {
@@ -6480,6 +6492,12 @@ component extends="modules.BaseModule" {
64806492
// Read response (handle both success and error streams)
64816493
var responseCode = conn.getResponseCode();
64826494
var inputStream = responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream();
6495+
// getErrorStream() returns Java null on a bodiless 4xx/5xx response;
6496+
// Scanner.init(null) NPEs on Lucee and surfaces as a useless "null"
6497+
// error message (#2947 review, #2977). No body — return empty.
6498+
if (isNull(inputStream)) {
6499+
return "";
6500+
}
64836501
var scanner = createObject("java", "java.util.Scanner").init(inputStream, "UTF-8");
64846502
var response = "";
64856503
while (scanner.hasNextLine()) {

‎vendor/wheels/Global.cfc‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2880,9 +2880,13 @@ return local.$wheels;
28802880
// fallback parsing attempts for common formats
28812881

28822882
// 1) ISO YYYY-MM-DD[ hh[:mm[:ss]]]
2883-
if (ReFind("(?i)^(\\d{4})-(\\d{2})-(\\d{2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?$", local.s2)) {
2884-
local.parts = ReReplace(local.s2, "^(\\d{4})-(\\d{2})-(\\d{2}).*$", "\\1-\\2-\\3", "all");
2885-
local.timePart = ReReplace(local.s2, ".*[ T](\\d{1,2}:\\d{2}(?::\\d{2})?).*$", "\\1", "all");
2883+
// Single-backslash escapes: in CFML "\\d" is a literal
2884+
// backslash + d in the compiled regex, which never matches a
2885+
// digit — the branch was dead. Mirrors the already-fixed
2886+
// slash-format branch below (#2933 carry-forward, #2977).
2887+
if (ReFind("(?i)^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$", local.s2)) {
2888+
local.parts = ReReplace(local.s2, "^(\d{4})-(\d{2})-(\d{2}).*$", "\1-\2-\3", "all");
2889+
local.timePart = ReReplace(local.s2, ".*[ T](\d{1,2}:\d{2}(?::\d{2})?).*$", "\1", "all");
28862890
if (Len(local.timePart) AND local.timePart NEQ local.s2) {
28872891
// has time
28882892
local.dt = ParseDateTime(local.parts & " " & local.timePart);

‎vendor/wheels/Public.cfc‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,9 @@ component output="false" displayName="Internal GUI" extends="wheels.Global" {
228228

229229
/**
230230
* Returns a struct { packages: [...], error: "" } populated from the
231-
* wheels-packages registry. Short-circuits in production (defense in
232-
* depth — the handler is already $blockInProduction()-gated). Captures
231+
* wheels-packages registry. Short-circuits outside development (defense in
232+
* depth — the handler is already $blockInProduction()-gated, which since
233+
* #2903 is a development-only allowlist). Captures
233234
* any registry error into the `error` field so the view can render a
234235
* friendly banner instead of a stack trace.
235236
*

‎vendor/wheels/controller/provides.cfc‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ component {
3030
/**
3131
* Use this in an individual controller action to define which formats the action will respond with.
3232
* This can be used to define provides behavior in individual actions or to override a global setting set with `provides` in the controller's `config()`.
33+
* Restrictions are enforced (since 4.0.4): `renderWith()` falls back to the `html` view for a
34+
* format outside the list, and the automatic render in `$callAction()` skips view rendering for
35+
* non-acceptable, non-html formats.
3336
*
3437
* [section: Controller]
3538
* [category: Provides Functions]

‎vendor/wheels/controller/rendering.cfc‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ component {
176176
* Instructs the controller to render the data passed in to the format that is requested.
177177
* If the format requested is `json` or `xml`, Wheels will transform the data into that format automatically.
178178
* For other formats (or to override the automatic formatting), you can also create a view template in this format: `nameofaction.xml.cfm`, `nameofaction.json.cfm`, `nameofaction.pdf.cfm`, etc.
179+
* Per-action format restrictions set with `onlyProvides()` are enforced here (since 4.0.4):
180+
* when the requested format is not acceptable for the action, `renderWith()` falls back to
181+
* rendering the `html` view — even when `html` itself is not in the `onlyProvides()` list.
179182
*
180183
* [section: Controller]
181184
* [category: Rendering Functions]

‎vendor/wheels/migrator/Base.cfc‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,16 @@ component extends="wheels.Global"{
109109
// rather than assigning a bare local inside catch: BoxLang discards
110110
// `local.X = ...` assignments made in a catch body.)
111111
local.state = {tableExists = true};
112-
local.quotedTable = StructKeyExists(this, "adapter") ? this.adapter.quoteTableName(arguments.table) : arguments.table;
112+
// Migration.init() always sets this.adapter, so a missing adapter is a
113+
// broken instantiation — fail loudly rather than silently interpolating
114+
// an UNQUOTED table name into SQL (#2937 review, #2977).
115+
if (!StructKeyExists(this, "adapter")) {
116+
Throw(
117+
type = "Wheels.Migrator.MissingAdapter",
118+
message = "$getForeignKeys() requires an initialized database adapter. Instantiate migrations through Migration.init()."
119+
);
120+
}
121+
local.quotedTable = this.adapter.quoteTableName(arguments.table);
113122
try {
114123
$query(
115124
datasource = application[local.appKey].dataSourceName,
@@ -206,7 +215,10 @@ component extends="wheels.Global"{
206215
// would otherwise issue a full table-metadata round-trip per row.
207216
// $execute() drops the cache whenever a statement runs, so DDL in the
208217
// same request (addColumn() etc.) is reflected on the next read.
209-
local.cacheKey = LCase(application[local.appKey].dataSourceName & "|" & arguments.tableName);
218+
// Key on the VERBATIM table name: the $dbinfo probe below uses original
219+
// case, so case-folding the key would let `Authors` and `authors` share
220+
// one slot on case-sensitive databases (#2937 review, #2977).
221+
local.cacheKey = application[local.appKey].dataSourceName & "|" & arguments.tableName;
210222
if (
211223
StructKeyExists(request, "$wheelsMigratorColumns")
212224
&& StructKeyExists(request.$wheelsMigratorColumns, local.cacheKey)

‎vendor/wheels/migrator/CLAUDE.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ The flag is read via `$get("useUnderscoreReferenceColumns")` inside `references(
5858
2. **Hard-coding `& "id"` or `& "type"` concatenations.** All four sites in this directory resolve the reference-column suffix through `$get("useUnderscoreReferenceColumns")` — `TableDefinition.cfc::references()` (id + polymorphic type), `Migration.cfc::removeColumn` (referenceName branch), and `Migration.cfc::addReference`. If you add new code that builds a reference column name, route it through `$get` too rather than hard-coding `& "id"`.
5959
3. **`required` on column-name parameters.** Use `$combineArguments(... required=true)` instead. Declaring CFML-level `required` blocks the alias path because validation runs before the function body.
6060

61+
## Internal caches
62+
63+
Two caches introduced in #2937 — know their scopes before adding probes:
64+
65+
- `application[appKey].$migratorAdapterNames` — application-scoped, keyed by datasource name. Memoized migrator adapter name, written by `Base.cfc::$getDBType()`. Survives requests; rebuilt on reload (a datasource's driver can't change without one).
66+
- `request.$wheelsMigratorColumns` — request-scoped, keyed by `dsName|tableName` (table name VERBATIM — no case folding, since the `$dbinfo` probe uses original case and case-sensitive databases can host `Authors` and `authors` separately). Column list per table, written by `Base.cfc::$getColumns()`, dropped wholesale by `$execute()` so DDL in the same request is reflected on the next read.
67+
6168
## Tests
6269

6370
Specs live in `vendor/wheels/tests/specs/migrator/`. `referencesSpec.cfc` exercises `TableDefinition::references()` (the `columnNames` alias plus the suffix flag) at the unit layer — inspecting `t.columns` / `t.foreignKeys` directly without `t.create()` so the assertions are adapter-independent. `primaryKeySpec.cfc` mirrors that shape for `TableDefinition::primaryKey()` — the `columnName` / `columnNames` aliases plus precedence semantics (#2803). `migrationSpec.cfc` covers Migration.cfc command-version helpers via real DDL roundtrips — its "Tests addReference" describe block guards the `useUnderscoreReferenceColumns` path on `Migration.cfc::addReference()`. Most FK-related tests in `migrationSpec.cfc` skip on SQLite (which doesn't support altering CONSTRAINTS) but run on every other engine in CI.

‎vendor/wheels/model/validations.cfc‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,11 @@ component {
962962
}
963963
local.leftOperand = IsNumeric(local.tokens[1]) ? JavaCast("double", local.tokens[1]) : local.tokens[1];
964964
local.rightOperand = IsNumeric(local.tokens[3]) ? JavaCast("double", local.tokens[3]) : local.tokens[3];
965-
return $resolveOperator(local.leftOperand, local.rightOperand, local.tokens[2]);
965+
// LCase keeps word-form operators ("1 EQ 0") compatible with the
966+
// case-sensitive switch in $resolveOperator on Adobe CF — symbolic
967+
// operators are already lowercased by $normalizeConditionOperators,
968+
// but word-form ones arrive raw (#2977).
969+
return $resolveOperator(local.leftOperand, local.rightOperand, LCase(local.tokens[2]));
966970
}
967971

968972
/**

0 commit comments

Comments
 (0)