abstract class Analyzer::Cfml::CfmlEngine

Overview

Shared CFML parsing layer.

See AGENTS.md §"Analyzer Layering": this owns file selection and the syntax handling every CFML adapter needs, so framework analyzers (Taffy, ColdBox, Wheels, FW/1) consume components and functions rather than re-implementing tag/script parsing.

The two syntaxes are the whole problem. CFML is written either as tags (<cffunction name="x" access="remote">) or as cfscript (remote string function x()), the two mix inside a single file, and everything is case-insensitive.

Direct Known Subclasses

Defined in:

analyzer/engines/cfml_engine.cr

Constant Summary

CFARGUMENT_TAG_RE = /<cfargument\b([\s\S]*?)>/i
CFCOMPONENT_TAG_RE = /<cfcomponent\b([\s\S]*?)>/i

Tags may span lines and pad their = for alignment, so attributes are matched with \s*=\s* and tag bodies with [\s\S]*?.

CFFUNCTION_TAG_RE = /<cffunction\b([\s\S]*?)>/i
HTTP_VERBS = Set {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}

Verbs a CFML framework can register. Route DSLs take verbs as free text, so a value outside this set is not turned into a method.

NAMED_ARGUMENT_RE = /\A([A-Za-z_]\w*)\s*[:=]\s*(.+)\z/m
SCRIPT_COMPONENT_HEADER_LIMIT = 2000

A component header is a handful of attributes; bound the scan so a file with no opening brace cannot walk the whole content.

SCRIPT_COMPONENT_RE = /(?<![\w.])component\b/i

component ... { header in script syntax, and a function declaration with its trailing attribute list up to the body brace.

SCRIPT_FUNCTION_RE = /(?<![\w.])function\s+(\w+)\s*\(/i
SPLIT_ARGUMENTS_RULES = Noir::TopLevelSplit::Rules.new(nest: (Noir::TopLevelSplit::Nest::Paren | Noir::TopLevelSplit::Nest::Bracket) | Noir::TopLevelSplit::Nest::Brace, quotes: "\"'", escape: Noir::TopLevelSplit::Escape::None, strip: true, empties: Noir::TopLevelSplit::Empties::DropAll, per_kind: false, clamp: false)

Rules::JAVA with Empties::DropAll and, like erlang/cowboy.cr and php/wordpress.cr, WITHOUT clamping: the body this replaces closed brackets with a bare depth -= 1, so an argument slice that opens on a closer drives depth negative and suppresses every later split. Observable on the unbalanced fragments the CFML regexes hand over, so it is preserved rather than normalised.

Escape::None because CFML has no backslash escape at all. "C:\log\" is the seven-character string C:\log\, and a quote is escaped by DOUBLING it ("say ""hi"""). Reading \ as an escape — which the hand-rolled body this replaced did, and which the rest of this engine did too until the same change — swallowed the closing quote of any string ending in a backslash, i.e. any Windows path, leaving the run open so the remainder of the argument list merged into the current part and every argument after it was lost.

There is deliberately no doubled-quote mode for the OTHER half of the rule, and that is not an oversight. For a top-level splitter "a repeated delimiter is a literal delimiter" and "quotes toggle" are indistinguishable: a doubled pair is two ADJACENT quote characters, so the toggling model's spurious closed-then-reopened window is zero characters wide. The state after any maximal run of quotes is the same under both models (a run of length L flips the state iff L is odd either way), and the only characters ever processed in a differing state are the quote characters themselves, which neither nest nor split. Checked by brute force rather than by argument: a doubling model run against TopLevelSplit over all 1,440 Rules combinations x 111,111 inputs (every string up to length 5 over " ' , a \ ( ) < backtick {) agreed on all 159,999,840 comparisons. matching_delimiter below implements doubling explicitly; that is equally correct and equally immaterial, and it is kept because a byte scanner reads more obviously with the rule written out.

File-local because this is the only splitter pairing clamp: false with DropAll.

TAG_ATTR_RE = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/

Attribute names carry : and - in the wild (taffy:uri, data-required), and values use either quote style.

TEST_COMPONENT_RE = /.+(?:Test|Spec)\.cfc\z/i

TestBox names suites <Something>Test.cfc / <Something>Spec.cfc. The leading .+ is load-bearing: a component named exactly Test.cfc is a demo, not a suite (fw1 ships one that declares a real remote method), and an anchored ends_with? swallowed it.

Instance methods inherited from class Analyzer

analyze analyze, base_path : String base_path, base_paths : Array(String) base_paths, base_relative_path(path : String) : String base_relative_path, callees_needed? : Bool callees_needed?, content_matches?(content : String, markers : Regex) : Bool content_matches?, http_header_name(name : String) : String | Nil http_header_name, line_number_for_index(content : String, char_index : Int32) : Int32 line_number_for_index, logger : NoirLogger logger, parallel_analyze(files : Array(String), &block : String -> Nil) parallel_analyze, read_file_content(path : String) : String read_file_content, result : Array(Endpoint) result, tech : String tech, unique_params(params : Array(Param)) : Array(Param) unique_params, url : String url, web_root_path(path : String, markers : Array(String)) : String web_root_path

Constructor methods inherited from class Analyzer

new(options : Hash(String, YAML::Any)) new

Macros inherited from class Analyzer

analyzer_for(tech) analyzer_for

Instance methods inherited from module FileHelper

all_files : Array(String) all_files, get_files_by_basename(basename : String) : Array(String) get_files_by_basename, get_files_by_extension(extension : String) : Array(String) get_files_by_extension, get_files_by_extensions(extensions : Array(String)) : Array(String) get_files_by_extensions, get_files_by_prefix(prefix : String) : Array(String) get_files_by_prefix, get_files_by_prefix_and_extension(prefix : String, extension : String) : Array(String) get_files_by_prefix_and_extension, get_files_by_relative_path(relative_path : String, root : String = "") : Array(String) get_files_by_relative_path, get_public_dir_files(base_path : String, folder : String) : Array(String) get_public_dir_files, get_public_files(base_path : String, anchors : Array(String) = ["shard.yml", "Gemfile"]) : Array(String) get_public_files, walked_path(expanded : String) : String walked_path