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
31 changes: 29 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,35 @@ a large file.
`cfmleditor/CFLint` is the main consumer. It declares the cfparser version **twice** — a
`cfparser.version` property in `pom.xml` and a hardcoded coordinate in `build.gradle`. Bump both.

It constructs a new `CFMLParser` per file, so the expression cache is per-file and never
accumulates across a scan.
It constructs **one** `CFMLParser` for the whole run — `private CFMLParser cfmlParser = new
CFMLParser();` is a field initialiser on `CFLint`. A 158-file scan builds exactly one. So the
expression cache accumulates across the entire scan, not per file.

(This paragraph previously said the opposite. Anything reasoning about per-file parser construction
cost — dictionary loading, cache locality — is reasoning about a pattern CFLint does not use.)

### What a CFLint scan actually costs

Almost all of it is ANTLR building its DFA the first time it meets each decision, not parsing.
Measured on 158 files in one JVM:

| | |
|---|---|
| first pass (cold DFA) | 4676 ms |
| second and third pass (warm) | 86 ms |

Same files, same process — **54×**. It is a fixed per-process cost, so scan time barely tracks
codebase size: 158 files took 5037 ms and 632 files took 5945 ms, a marginal ~2 ms per file.

Two consequences worth keeping in mind:

- Caching and allocation work inside cfparser will not move a CFLint CLI scan. Profiling one shows
~91% of samples in ANTLR, `parseScriptBlock` accounting for 74% of wall clock, the SLL→LL fallback
firing zero times, and the expression LRU taking zero hits.
- A long-lived consumer (daemon, language server, IDE) amortises the warm-up completely and is the
single biggest available win. Conversely `clearDFA()` throws it away — and it is process-global
(`_decisionToDFA` is `static final` on the generated parser), so it clears every parser instance
in the JVM, not just the one it was called on.

CFLint must stay on the same Java baseline. Its artifacts cannot load class file version 65 on an
older JVM.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,16 @@ public static SyntaxDictionary getDictionaryByVersion(String versionkey) {
if (dictionaryConfig == null)
throw new IllegalArgumentException("Problem loading dictionaryconfig.xml");

// The parsed dictionary is immutable once loaded and every CFMLParser construction
// asks for the same version, so re-parsing the XML each time is pure waste. Key the
// cache on the preference signature as well so a later initDictionaries(prefs) with
// different preferences still gets a freshly loaded dictionary.
final String versionCacheKey = versionkey + "\u0000" + prefsSignature(fPrefs);
final SyntaxDictionary cachedDictionary = (SyntaxDictionary) dictionaryVersionCache.get(versionCacheKey);
if (cachedDictionary != null) {
return cachedDictionary;
}

// grab the cfml dictionary
// Node n = dictionaryConfig.getElementById(CFDIC).getFirstChild();
Node versionNode = dictionaryConfig.getElementById(versionkey);
Expand Down Expand Up @@ -400,6 +410,7 @@ public static SyntaxDictionary getDictionaryByVersion(String versionkey) {
String filename = n.getAttributes().getNamedItem("location").getNodeValue().trim();
dic.loadDictionary(getDictionaryLocation(filename));
}
dictionaryVersionCache.put(versionCacheKey, dic);
return dic;
}

Expand Down
12 changes: 7 additions & 5 deletions cfml.parsing/src/main/java/cfml/parsing/CFMLParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,8 @@ public void visit(final Element elem, final int level, CFMLVisitor visitor) thro
// <cfset a = 1 /> would otherwise be parsed as the expression "a = 1 /".
// An expression can never legitimately end in '/' - division needs a right
// operand - so removing a trailing one is safe.
String cfscript = elem.toString().substring(elem.getName().length() + 1, elem.toString().length() - 1)
final String elemText = elem.toString();
String cfscript = elemText.substring(elem.getName().length() + 1, elemText.length() - 1)
.trim();
if (cfscript.endsWith("/")) {
cfscript = cfscript.substring(0, cfscript.length() - 1).trim();
Expand All @@ -295,21 +296,22 @@ public void visit(final Element elem, final int level, CFMLVisitor visitor) thro
// added twice, reading past the end of the tag (and out of the string entirely once
// the element sits far enough into the file).
final int elemBegin = elem.getBegin();
final int uglyNotPos = elem.toString().lastIndexOf("<>");
final String elemText = elem.toString();
final int uglyNotPos = elemText.lastIndexOf("<>");
int endPos = elem.getStartTag().getEnd() - 1 - elemBegin;

if (uglyNotPos > 0) {
final int nextPos = elem.toString().indexOf(">", uglyNotPos + 2);
final int nextPos = elemText.indexOf(">", uglyNotPos + 2);
// An unclosed or self-closing tag has no end tag; fall back to the element's own
// extent rather than dereferencing null.
final int endTagBegin = elem.getEndTag() == null ? elem.toString().length()
final int endTagBegin = elem.getEndTag() == null ? elemText.length()
: elem.getEndTag().getBegin() - elemBegin;
if (nextPos > 0 && nextPos < endTagBegin) {
endPos = nextPos;
}
}

final String cfscript = elem.toString().substring(elem.getName().length() + 1, endPos);
final String cfscript = elemText.substring(elem.getName().length() + 1, endPos);
if (cfscript.length() > 0 && visitor.visitPreParseExpression("TAG", cfscript)) {
final CFExpression expression = parseCFExpression(cfscript, visitor);

Expand Down
Loading