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
57 changes: 45 additions & 12 deletions tools/code-quality/cfml-coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,30 @@
import os, re, sys, json, shutil, argparse

SCRIPT_FN = re.compile(r'\bfunction\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{', re.S)
TAG_FN = re.compile(r'(<cffunction\b[^>]*?\bname\s*=\s*["\']?[A-Za-z_$][\w$]*[^>]*?>)', re.I)
# Guarded closure assignments (include-idempotent templates):
# variables.$helper = function(args) { ... };
CLOSURE_FN = re.compile(r'\bvariables\s*\.\s*\$?([A-Za-z_$][\w$]*)\s*=\s*function\s*\([^)]*\)\s*\{', re.S)
# Capture the name only (group 1); the opening tag itself is not part of the id —
# embedding it produced ids full of quotes, i.e. invalid CFML in the counter.
TAG_FN = re.compile(r'<cffunction\b[^>]*?\bname\s*=\s*["\']?([A-Za-z_$][\w$]*)', re.I)
EXCLUDE_DIRS = {'tests', 'rocketunit_tests'}

DSTRING = re.compile(r'"(?:[^"\\]|\\.)*"', re.S)
SSTRING = re.compile(r"'(?:[^'\\]|\\.)*'", re.S)
LINE_COMMENT = re.compile(r'//[^\n]*')
BLOCK_COMMENT = re.compile(r'/\*.*?\*/', re.S)
TAG_COMMENT = re.compile(r'<!---.*?--->', re.S)


def _mask(text):
"""Mask strings and comments with same-length spaces so function patterns
never match inside them (JS `function name(){` in a CFML string literal
used to get a counter inserted mid-string — a parse error). Offsets are
preserved, so matches map back onto the original text unchanged."""
for pat in (DSTRING, SSTRING, LINE_COMMENT, BLOCK_COMMENT, TAG_COMMENT):
text = pat.sub(lambda m: ' ' * len(m.group(0)), text)
return text


def _backup_dir(root):
"""Sibling of the instrumented root, so the walk never re-instruments it."""
Expand Down Expand Up @@ -59,17 +80,23 @@ def instrument(root):
with open(path, encoding='utf-8', errors='replace') as fh:
text = fh.read()
rel = _rel(path, root)
# script functions: insert a script-syntax counter right after '{'
masked = _mask(text)
# Collect every insertion point against the original (masked) offsets
# first, then apply them right-to-left so earlier insertions never
# shift later positions.
matches = []
for pat in (SCRIPT_FN, CLOSURE_FN):
for m in pat.finditer(masked):
matches.append((m.end(), 'script', m.group(1)))
for m in TAG_FN.finditer(masked):
brace = masked.find('>', m.end())
if brace >= 0:
matches.append((brace + 1, 'tag', m.group(1)))
matches.sort(key=lambda t: -t[0])
out = text
for m in reversed(list(SCRIPT_FN.finditer(text))):
brace = m.end() # position just after '{'
ctr = SCRIPT_COUNTER.format(id=f"{rel}:{m.group(1)}")
out = out[:brace] + ctr + out[brace:]
n += 1
# tag functions: insert a <cfset> counter right after the opening tag
for m in reversed(list(TAG_FN.finditer(out))):
ctr = TAG_COUNTER.format(id=f"{rel}:{m.group(1)}")
out = out[:m.end()] + ctr + out[m.end():]
for pos, kind, name in matches:
ctr = (SCRIPT_COUNTER if kind == 'script' else TAG_COUNTER).format(id=f"{rel}:{name}")
out = out[:pos] + ctr + out[pos:]
n += 1
if out != text:
backup = os.path.join(_backup_dir(root), rel)
Expand Down Expand Up @@ -104,7 +131,13 @@ def _cov_key(complexity_id):

def combine(root, coverage_path, complexity_path):
cov = json.load(open(coverage_path))
complexity = {r['id']: r['complexity'] for r in json.load(open(complexity_path))}
raw_complexity = json.load(open(complexity_path))
if isinstance(raw_complexity, dict):
# cfml-complexity.py --baseline write emits {id: complexity}
complexity = {rid: int(comp) for rid, comp in raw_complexity.items()}
else:
# --json emits a list of rows with id/complexity
complexity = {r['id']: int(r['complexity']) for r in raw_complexity}
# cov keys are "<rel>:<fn>"; complexity ids are "<rel>:<line>:<fn>"
rows = []
for cid, comp in complexity.items():
Expand Down
21 changes: 18 additions & 3 deletions vendor/wheels/JobWorker.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,18 @@ component {
local.recentParams.queue = {value = arguments.queue, cfsqltype = "cf_sql_varchar"};
}
local.recentSql &= " ORDER BY updatedAt DESC";
local.recentRows = queryExecute(local.recentSql, local.recentParams, {datasource = variables.$datasource, maxrows = 10});
// Bound in SQL text via the dialect clause, not the driver maxrows
// option: BoxLang's JDBC layer calls setLargeMaxRows(), which the
// PostgreSQL driver does not implement ("is not yet implemented") —
// the query throws and the catch below returned an empty
// recentJobs. The CFML break is a belt-and-braces backstop.
local.recentSql &= $candidateLimitClause(dbType = $dbType(), candidateLimit = 10);
local.recentRows = queryExecute(local.recentSql, local.recentParams, {datasource = variables.$datasource});

for (local.row in local.recentRows) {
if (ArrayLen(local.result.recentJobs) >= 10) {
break;
}
ArrayAppend(local.result.recentJobs, {
id = local.row.id,
jobClass = local.row.jobClass,
Expand All @@ -302,7 +311,10 @@ component {
local.oldestParams.queue = {value = arguments.queue, cfsqltype = "cf_sql_varchar"};
}
local.oldestSql &= " ORDER BY createdAt ASC";
local.oldestRow = queryExecute(local.oldestSql, local.oldestParams, {datasource = variables.$datasource, maxrows = 1});
// Bound in SQL text (see the recentJobs note on driver maxrows) —
// the ORDER BY puts the oldest pending row first.
local.oldestSql &= $candidateLimitClause(dbType = $dbType(), candidateLimit = 1);
local.oldestRow = queryExecute(local.oldestSql, local.oldestParams, {datasource = variables.$datasource});
if (local.oldestRow.recordCount) {
// This reads through a raw queryExecute, so the Wheels adapter's
// date canonicalization never runs. Adobe's JDBC driver returns
Expand Down Expand Up @@ -339,7 +351,10 @@ component {
local.selectParams.queue = {value = arguments.queue, cfsqltype = "cf_sql_varchar"};
}
local.selectSql &= " ORDER BY failedAt ASC";
local.failedJobs = queryExecute(local.selectSql, local.selectParams, {datasource = variables.$datasource, maxrows = arguments.limit});
// Bound in SQL text (BoxLang + PostgreSQL throws on the driver
// maxrows option — setLargeMaxRows is not implemented).
local.selectSql &= $candidateLimitClause(dbType = $dbType(), candidateLimit = arguments.limit);
local.failedJobs = queryExecute(local.selectSql, local.selectParams, {datasource = variables.$datasource});

if (!local.failedJobs.recordCount) return 0;

Expand Down
24 changes: 12 additions & 12 deletions vendor/wheels/model/sql.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1213,18 +1213,18 @@ component {
}
if (Find("'", local.processedValue) > 0 || Find(Chr(34), local.processedValue) > 0) {
local.cleanedValue = local.processedValue;
// Unquote token delimiters only: fold quote-comma-quote
// separators into plain commas first (IN-lists), then strip
// one pair of outer quotes. The previous `'([^']*)'`
// regex-strip ate inner apostrophes — "O'Brien" came back
// as "OBrien" with a leaked trailing quote.
local.cleanedValue = ReReplace(local.cleanedValue, "'\s*,\s*'", ",", "ALL");
local.doubleQuote = Chr(34);
local.cleanedValue = ReReplace(local.cleanedValue, "#local.doubleQuote#\s*,\s*#local.doubleQuote#", ",", "ALL");
local.cleanedValue = ReReplace(local.cleanedValue, "^'", "", "ONE");
local.cleanedValue = ReReplace(local.cleanedValue, "'$", "", "ONE");
local.cleanedValue = ReReplace(local.cleanedValue, "^#local.doubleQuote#", "", "ONE");
local.cleanedValue = ReReplace(local.cleanedValue, "#local.doubleQuote#$", "", "ONE");
// IN-lists keep their quoted items verbatim (byte-identical
// to the Lucee/Adobe ReplaceList path — the list branch of
// $queryParams unquotes via $cleanInStatementValue).
// Single values drop exactly one pair of outer quotes so
// inner apostrophes survive ("O'Brien" — the previous
// regex-strip ate them and seedOnce re-created rows).
if (!(Find("','", local.cleanedValue) > 0 || Find("#Chr(34)#,#Chr(34)#", local.cleanedValue) > 0)) {
local.cleanedValue = ReReplace(local.cleanedValue, "^'", "", "ONE");
local.cleanedValue = ReReplace(local.cleanedValue, "'$", "", "ONE");
local.cleanedValue = ReReplace(local.cleanedValue, "^#Chr(34)#", "", "ONE");
local.cleanedValue = ReReplace(local.cleanedValue, "#Chr(34)#$", "", "ONE");
}
ArrayAppend(local.originalValues, local.cleanedValue);
} else {
ArrayAppend(local.originalValues, local.processedValue);
Expand Down
Loading