* Delegate a ` ... ` tag body to the cfscript grammar. * With `parentClassName` set (the block sits at component scope), functions * declared at the script's top level are the component's methods * (` function configure(){}` — the standard ColdBox
(scriptTag: SyntaxNode, parentId: string | undefined, parentClassName?: string)
| 333 | * (closures) keep kind `function`. |
| 334 | */ |
| 335 | private delegateScriptTag(scriptTag: SyntaxNode, parentId: string | undefined, parentClassName?: string): void { |
| 336 | const content = scriptTag.namedChildren.find((c: SyntaxNode) => c.type === 'cf_script_content'); |
| 337 | if (!content) return; |
| 338 | |
| 339 | const inner = this.source.substring(content.startIndex, content.endIndex); |
| 340 | const startLine = content.startPosition.row; |
| 341 | |
| 342 | const extractor = new TreeSitterExtractor(this.filePath, inner, 'cfscript'); |
| 343 | const result = extractor.extract(); |
| 344 | |
| 345 | // The inner TreeSitterExtractor always synthesizes its own `file`-kind |
| 346 | // node scoped to just this snippet — drop it (and any edges touching it) |
| 347 | // since this tag-based file already owns one correctly-ranged file node |
| 348 | // (see createFileNode); the per-node `parentId` contains-edge below |
| 349 | // already links every emitted symbol into the real tree. |
| 350 | const innerFileNodeId = result.nodes.find((n) => n.kind === 'file')?.id; |
| 351 | // Snippet-top-level symbols are the ones the inner extractor attached |
| 352 | // directly to its (dropped) snippet file node — as opposed to closures |
| 353 | // nested inside another function. |
| 354 | const topLevelIds = new Set( |
| 355 | result.edges |
| 356 | .filter((e) => e.kind === 'contains' && e.source === innerFileNodeId) |
| 357 | .map((e) => e.target) |
| 358 | ); |
| 359 | for (const node of result.nodes) { |
| 360 | if (node.kind === 'file') continue; |
| 361 | node.startLine += startLine; |
| 362 | node.endLine += startLine; |
| 363 | node.language = this.language; |
| 364 | if (parentClassName) { |
| 365 | if (node.kind === 'function' && topLevelIds.has(node.id)) { |
| 366 | node.kind = 'method'; |
| 367 | } |
| 368 | node.qualifiedName = `${parentClassName}::${node.qualifiedName}`; |
| 369 | } |
| 370 | this.nodes.push(node); |
| 371 | if (parentId) { |
| 372 | this.edges.push({ source: parentId, target: node.id, kind: 'contains' }); |
| 373 | } |
| 374 | } |
| 375 | for (const edge of result.edges) { |
| 376 | if (edge.source === innerFileNodeId || edge.target === innerFileNodeId) continue; |
| 377 | if (edge.line) edge.line += startLine; |
| 378 | this.edges.push(edge); |
| 379 | } |
| 380 | for (const ref of result.unresolvedReferences) { |
| 381 | ref.line += startLine; |
| 382 | ref.filePath = this.filePath; |
| 383 | ref.language = this.language; |
| 384 | // Calls inside a <cfscript> body with no enclosing function (rare — a |
| 385 | // top-level script in a .cfm template, or any statement directly in |
| 386 | // the snippet body) attribute to the filtered-out snippet file node by |
| 387 | // default — redirect those (and any genuinely unset ones) to parentId. |
| 388 | if ((!ref.fromNodeId || ref.fromNodeId === innerFileNodeId) && parentId) ref.fromNodeId = parentId; |
| 389 | this.unresolvedReferences.push(ref); |
| 390 | } |
| 391 | for (const error of result.errors) { |
| 392 | if (error.line) error.line += startLine; |
no test coverage detected