* Resolve a CFML inheritance reference written as a component path (#1152). * Two forms exist in real code: * * - Dotted: `extends="coldbox.system.web.Controller"` — dots are directory * separators from the webroot or a CFML mapping. Mappings live in server * config / Application.
(ref: UnresolvedRef)
| 2180 | * dotted paths are written to match the on-disk file name. |
| 2181 | */ |
| 2182 | private resolveCfmlComponentPath(ref: UnresolvedRef): ResolvedRef | null { |
| 2183 | const cfmlCandidates = (name: string): Node[] => |
| 2184 | this.context |
| 2185 | .getNodesByName(name) |
| 2186 | .filter( |
| 2187 | (n) => |
| 2188 | (n.kind === 'class' || n.kind === 'interface') && |
| 2189 | (n.language === 'cfml' || n.language === 'cfscript') |
| 2190 | ); |
| 2191 | const norm = (p: string): string => p.replace(/\\/g, '/').toLowerCase(); |
| 2192 | |
| 2193 | // Relative-path form: `../base`, `./base`, `sub/thing` — resolve against |
| 2194 | // the referencing file's directory and require an exact (case-insensitive) |
| 2195 | // file match. |
| 2196 | if (ref.referenceName.includes('/')) { |
| 2197 | const rel = ref.referenceName.replace(/\.cfc$/i, ''); |
| 2198 | const fromDir = ref.filePath.replace(/\\/g, '/').split('/').slice(0, -1); |
| 2199 | const parts = [...fromDir]; |
| 2200 | for (const seg of rel.split('/')) { |
| 2201 | if (seg === '' || seg === '.') continue; |
| 2202 | if (seg === '..') { |
| 2203 | if (parts.length === 0) return null; // escapes the project root |
| 2204 | parts.pop(); |
| 2205 | } else { |
| 2206 | parts.push(seg); |
| 2207 | } |
| 2208 | } |
| 2209 | const wantPath = norm(parts.join('/') + '.cfc'); |
| 2210 | const className = parts[parts.length - 1]; |
| 2211 | if (!className) return null; |
| 2212 | const target = cfmlCandidates(className).find((c) => norm(c.filePath) === wantPath); |
| 2213 | return target |
| 2214 | ? { original: ref, targetNodeId: target.id, confidence: 0.95, resolvedBy: 'file-path' } |
| 2215 | : null; |
| 2216 | } |
| 2217 | |
| 2218 | // Dotted form. |
| 2219 | const segments = ref.referenceName.split('.').map((s) => s.trim()).filter(Boolean); |
| 2220 | if (segments.length < 2) return null; |
| 2221 | const className = segments[segments.length - 1]!; |
| 2222 | const dirSegments = segments.slice(0, -1); |
| 2223 | |
| 2224 | let best: Node | null = null; |
| 2225 | let bestScore = 0; |
| 2226 | let tie = false; |
| 2227 | for (const cand of cfmlCandidates(className)) { |
| 2228 | const dirs = cand.filePath.replace(/\\/g, '/').split('/').slice(0, -1); |
| 2229 | // Count matching directory segments right-to-left: for |
| 2230 | // `coldbox.system.web.Controller` vs `system/web/Controller.cfc`, |
| 2231 | // `web` and `system` match, then the repo root ends the run → score 2. |
| 2232 | let score = 0; |
| 2233 | while ( |
| 2234 | score < dirSegments.length && |
| 2235 | score < dirs.length && |
| 2236 | dirSegments[dirSegments.length - 1 - score]!.toLowerCase() === |
| 2237 | dirs[dirs.length - 1 - score]!.toLowerCase() |
| 2238 | ) { |
| 2239 | score++; |
no test coverage detected