Tag-based and bare-script CFML, extends/implements, <cfscript>/<cfquery> delegation, BOM + unquoted-attribute handling. Wasm grammars verified bit-for-bit reproducible from cfmleditor/tree-sitter-cfml. Validated on FW/1, ColdBox, CFWheels. Follow-up: #1152. Co-authored-by: ghedwards <125586+ghedwards@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
|
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
|
|
import type { LanguageExtractor } from '../tree-sitter-types';
|
|
|
|
/** CFML access modifiers (`public`/`private`/`package`/`remote`) on a function_declaration. */
|
|
function cfmlVisibility(node: SyntaxNode): 'public' | 'private' | 'protected' | 'internal' | undefined {
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
const child = node.child(i);
|
|
if (child?.type === 'access_type') {
|
|
const text = child.text;
|
|
if (text === 'public') return 'public';
|
|
if (text === 'private') return 'private';
|
|
if (text === 'package') return 'internal';
|
|
if (text === 'remote') return 'public';
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export const cfscriptExtractor: LanguageExtractor = {
|
|
functionTypes: ['function_declaration', 'function_expression', 'arrow_function'],
|
|
classTypes: ['component'],
|
|
// `component` is reused for both `component { ... }` and `interface { ... }` —
|
|
// the only difference is the literal first token (verified via the grammar's
|
|
// native binding: child(0).type is 'component' or 'interface', both unnamed).
|
|
classifyClassNode: (node) => (node.child(0)?.type === 'interface' ? 'interface' : 'class'),
|
|
methodTypes: ['function_declaration', 'method_definition'],
|
|
interfaceTypes: [],
|
|
structTypes: [],
|
|
enumTypes: [],
|
|
typeAliasTypes: [],
|
|
importTypes: ['import_statement', 'include_statement'],
|
|
callTypes: ['call_expression'],
|
|
variableTypes: ['variable_declaration'],
|
|
propertyTypes: ['property_declaration'],
|
|
nameField: 'name',
|
|
bodyField: 'body',
|
|
paramsField: 'parameters',
|
|
getVisibility: cfmlVisibility,
|
|
getSignature: (node, source) => {
|
|
const params = getChildByField(node, 'parameters');
|
|
return params ? getNodeText(params, source) : undefined;
|
|
},
|
|
extractImport: (node, source) => {
|
|
const importText = source.substring(node.startIndex, node.endIndex).trim();
|
|
|
|
if (node.type === 'include_statement') {
|
|
// `include "path/to/file.cfm";` — the included template path.
|
|
const expr = node.namedChildren.find((c: SyntaxNode) => c.type === 'string');
|
|
if (!expr) return null;
|
|
const moduleName = getNodeText(expr, source).replace(/^["']|["']$/g, '');
|
|
return moduleName ? { moduleName, signature: importText } : null;
|
|
}
|
|
|
|
// `import com.foo.Bar;` (dotted path) or `import "java:java.util.ArrayList";` (string form)
|
|
const sourceNode = getChildByField(node, 'source');
|
|
if (!sourceNode) return null;
|
|
|
|
let moduleName: string;
|
|
if (sourceNode.type === 'import_path') {
|
|
moduleName = sourceNode.namedChildren
|
|
.map((c: SyntaxNode) => getNodeText(c, source))
|
|
.join('.');
|
|
} else {
|
|
moduleName = getNodeText(sourceNode, source).replace(/^["']|["']$/g, '');
|
|
}
|
|
return moduleName ? { moduleName, signature: importText } : null;
|
|
},
|
|
};
|