Skip to content

Give cfproperty and cfparam their real attribute sets - #62

Merged
ghedwards merged 1 commit into
masterfrom
claude/fix-attribute-allowlists-43
Aug 13, 2026
Merged

Give cfproperty and cfparam their real attribute sets#62
ghedwards merged 1 commit into
masterfrom
claude/fix-attribute-allowlists-43

Conversation

@ghedwards

Copy link
Copy Markdown

Resolves the question #43 left open, by answering it with evidence rather than judgement. Refs #39.

The blocker was never the wiring

#43 deferred "should validateAttributes be wired up for param and property?" on the grounds that it throws. Going to answer that, I checked whether the allowed sets were even correct. They are not.

Class Allowed set Dictionary says
CFLockStatement 5 5 exact match — and it is the one live caller
CFParamStatement 6 7 missing MAXLENGTH
CFPropertyStatement 6 65 holds cfparam's six

CFPropertyStatement.validAttributes was NAME, TYPE, DEFAULT, MAX, MIN, PATTERN — a copy of cfparam's list. Only the first three are cfproperty attributes at all. So getter, setter, persistent, fieldtype, ormtype, required, hint — the attributes on essentially every accessor-generating or persistent CFC — all counted as invalid.

That reframes the original question entirely. Wiring property validation up would not have been a debatable trade; it would have rejected almost every ORM entity ever written.

getValidAttributes() is public static on both classes, so this was reachable from outside regardless of wiring. That makes it a live bug, not dormant code.

Both sets now come from the dictionaries

The shipped dictionaries agree on all three tags across cf11 and lucee5 — no dialect divergence — so a hardcoded set can be correct for both:

cflock:     cf11=5  lucee5=5   (no difference)
cfparam:    cf11=7  lucee5=7   (no difference)
cfproperty: cf11=65 lucee5=65  (no difference)

CFLockStatement matching the dictionary exactly is the useful control here: the validation mechanism was never broken, only the data it was given.

Still not wired up, and now for a better reason

validateAttributes throws, parseScript does not catch, so one unrecognised attribute aborts the whole file instead of reporting an error on the statement. There is no error-reporting path through CFScriptStatementVisitor to change that without real plumbing.

The decisive argument is staleness: these sets are pinned to dictionaries that ship with the parser, and will fall behind whatever the next CF release adds. A hardcoded allowlist that throws is a time bomb aimed at valid code, in a parser whose job is to survive unfamiliar input. Correct data makes that failure rarer, not impossible.

So the sets are now right for anyone who asks — including CFLint, which has the dictionary and a proper diagnostic channel — and the parser still does not abort files over an attribute it has not heard of.

cfadmin is deliberately untouched: its set carries RETURNVARIABLE, which the lucee4.5 dictionary omits but Lucee accepts. A permissive superset, not a bug.

Tests

Four added. Three fail with cfml.parsing/src/main stashed:

testPropertyAndParamSetsAreNotTheSame  expected:<7> but was:<6>
testParamAllowsMaxLength               ParseException: Invalid attribute maxlength
testPropertyAllowsItsOwnAttributes     ParseException: Invalid attribute getter

The fourth, testPropertyStillRejectsUnknownAttributes, passes either way by design — it guards the rejection path so a future widening of the set cannot quietly turn validation into a no-op.

Verification

  • 322 tests, ./gradlew build
  • CFLint 675 against a clean build

Generated by Claude Code

CFPropertyStatement's validAttributes held cfparam's six -- name, type,
default, max, min, pattern. cfproperty accepts 65. Every ORM and bean
property in real code carries getter, setter, persistent, fieldtype or
ormtype, and all of them counted as invalid. getValidAttributes() is public
static, so consumers asking cfparser what a property may carry were handed
cfparam's answer.

CFParamStatement was missing MAXLENGTH, so param name="x" maxlength="5"
counted as invalid too.

Both sets now come from the shipped dictionaries, which agree on all three
tags across cf11 and lucee5, so a hardcoded set can be right for both
dialects. CFLockStatement is the one place validateAttributes is actually
called, and its five match the dictionary exactly -- the mechanism was never
the problem, the data was.

## Still not wired up

#39 left open whether to call validateAttributes for param and property. The
answer is no, and the sets above are why it can now be argued rather than
guessed. validateAttributes throws and parseScript does not catch, so one
unrecognised attribute aborts the whole file rather than reporting an error
on the statement. These sets are pinned to dictionaries that ship with the
parser and will fall behind whatever the next CF release adds -- so that
abort would eventually land on valid code, in a parser whose job is to
survive unfamiliar input. The wrong sets made this a much bigger hazard than
it looked: enabling property validation would have rejected almost every
persistent CFC ever written.

cfadmin is left alone. Its set carries RETURNVARIABLE, which the lucee4.5
dictionary omits but Lucee does accept, so it is a permissive superset rather
than a bug.

322 tests, ./gradlew build, CFLint's 675 against a clean build. Three of the
four new tests fail with cfml.parsing/src/main stashed; the fourth pins that
unknown attributes are still rejected, so it passes either way by design.

Refs #39, #43
@ghedwards
ghedwards merged commit 3e7b76e into master Aug 13, 2026
3 checks passed
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.

2 participants