1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import * as arkts from '@koalaui/libarkts'; 17import { AbstractVisitor, VisitorOptions } from './abstract-visitor'; 18import { matchPrefix } from './arkts-utils'; 19import { debugDump, getDumpFileName } from './debug'; 20import { InteroperAbilityNames, ARKUI_COMPONENT_IMPORT_NAME, KIT_ARKUI_NAME } from './predefines'; 21import { PluginContext } from './plugin-context'; 22import { LegacyTransformer } from '../ui-plugins/legacy-transformer'; 23import { ComponentTransformer } from '../ui-plugins/component-transformer'; 24 25export interface ProgramVisitorOptions extends VisitorOptions { 26 pluginName: string; 27 state: arkts.Es2pandaContextState; 28 visitors: AbstractVisitor[]; 29 skipPrefixNames: (string | RegExp)[]; 30 hooks?: ProgramHooks; 31 pluginContext?: PluginContext; 32} 33 34export interface ProgramHookConfig { 35 visitors: AbstractVisitor[]; 36 resetAfter?: arkts.Es2pandaContextState; 37} 38 39export type ProgramHookLifeCycle = Partial<Record<'pre' | 'post', ProgramHookConfig>>; 40 41export interface ProgramHooks { 42 external?: ProgramHookLifeCycle; 43 source?: ProgramHookLifeCycle; 44} 45 46function flattenVisitorsInHooks( 47 programHooks?: ProgramHooks, 48 resetAfterValue?: arkts.Es2pandaContextState 49): AbstractVisitor[] { 50 if (!programHooks) return []; 51 const flatMapInHook = (config: ProgramHookConfig): AbstractVisitor[] => { 52 if (!resetAfterValue) return []; 53 if (!config.resetAfter || resetAfterValue !== config.resetAfter) return []; 54 return config.visitors; 55 }; 56 return [ 57 ...Object.values(programHooks.external || {}).flatMap(flatMapInHook), 58 ...Object.values(programHooks.source || {}).flatMap(flatMapInHook), 59 ]; 60} 61 62function sortExternalSources(externalSources: arkts.ExternalSource[]): arkts.ExternalSource[] { 63 return externalSources.sort((a, b) => { 64 const prefix = ARKUI_COMPONENT_IMPORT_NAME || KIT_ARKUI_NAME; 65 const hasPrefixA = a.getName().startsWith(prefix); 66 const hasPrefixB = b.getName().startsWith(prefix); 67 68 // If both have the prefix, maintain their original order 69 if (hasPrefixA && hasPrefixB) { 70 return 0; 71 } 72 // If neither has the prefix, maintain their original order 73 if (!hasPrefixA && !hasPrefixB) { 74 return 0; 75 } 76 // If only one has the prefix, the one with the prefix comes first 77 return hasPrefixA ? -1 : 1; 78 }); 79} 80 81export interface StructMap { 82 [key: string]: string; 83} 84 85export class ProgramVisitor extends AbstractVisitor { 86 private readonly pluginName: string; 87 private readonly state: arkts.Es2pandaContextState; 88 private readonly visitors: AbstractVisitor[]; 89 private readonly skipPrefixNames: (string | RegExp)[]; 90 private readonly hooks?: ProgramHooks; 91 private filenames: Map<number, string>; 92 private pluginContext?: PluginContext; 93 private legacyModuleList: string[] = []; 94 private legacyStructMap: Map<string, StructMap>; 95 96 constructor(options: ProgramVisitorOptions) { 97 super(options); 98 this.pluginName = options.pluginName; 99 this.state = options.state; 100 this.visitors = options.visitors; 101 this.skipPrefixNames = options.skipPrefixNames ?? []; 102 this.hooks = options.hooks; 103 this.filenames = new Map(); 104 this.pluginContext = options.pluginContext; 105 this.legacyModuleList = []; 106 this.legacyStructMap = new Map(); 107 } 108 109 reset(): void { 110 super.reset(); 111 this.filenames = new Map(); 112 this.legacyStructMap = new Map(); 113 this.legacyModuleList = []; 114 } 115 116 getLegacyModule(): void { 117 const moduleList = this.pluginContext?.getProjectConfig()?.dependentModuleList; 118 if (moduleList === undefined) { 119 return; 120 } 121 for (const module of moduleList) { 122 const language = module.language; 123 const moduleName = module.moduleName; 124 if (language !== InteroperAbilityNames.ARKTS_1_1) { 125 continue; 126 } 127 if (!this.legacyStructMap.has(moduleName)) { 128 this.legacyStructMap.set(moduleName, {}); 129 this.legacyModuleList.push(moduleName); 130 } 131 } 132 } 133 134 dumpHeaders( 135 currProgram: arkts.Program, 136 name: string, 137 cachePath: string | undefined, 138 prefixName: string, 139 extensionName: string 140 ): void { 141 debugDump( 142 currProgram.astNode.dumpSrc(), 143 getDumpFileName(this.state, prefixName, undefined, name), 144 true, 145 cachePath, 146 extensionName 147 ); 148 } 149 150 programVisitor(program: arkts.Program): arkts.Program { 151 const skipPrefixes: (string | RegExp)[] = this.skipPrefixNames; 152 const visited = new Set(); 153 const queue: arkts.Program[] = [program]; 154 this.getLegacyModule(); 155 while (queue.length > 0) { 156 const currProgram = queue.shift()!; 157 if (visited.has(currProgram.peer)) { 158 continue; 159 } 160 if (currProgram.peer !== program.peer) { 161 const name: string = this.filenames.get(currProgram.peer)!; 162 const cachePath: string | undefined = this.pluginContext?.getProjectConfig()?.cachePath; 163 if (this.legacyModuleList && matchPrefix(this.legacyModuleList, name)) { 164 if (this.state === arkts.Es2pandaContextState.ES2PANDA_STATE_PARSED) { 165 const structList = this.visitorLegacy(currProgram.astNode, currProgram, name); 166 const moduleName = name.split('/')[0]; 167 const structMap = this.legacyStructMap.get(moduleName)!; 168 for (const struct of structList) { 169 structMap[struct] = name; 170 } 171 } 172 } else { 173 this.dumpHeaders(currProgram, name, cachePath, 'Ori', program.programFileNameWithExtension); 174 const script = this.visitor(currProgram.astNode, currProgram, name); 175 if (script) { 176 this.dumpHeaders(currProgram, name, cachePath, this.pluginName, program.programFileNameWithExtension); 177 } 178 } 179 } 180 visited.add(currProgram.peer); 181 for (const externalSource of sortExternalSources(currProgram.externalSources)) { 182 if (matchPrefix(skipPrefixes, externalSource.getName())) { 183 continue; 184 } 185 const nextProgramArr: arkts.Program[] = externalSource.programs ?? []; 186 for (const nextProgram of nextProgramArr) { 187 this.filenames.set(nextProgram.peer, externalSource.getName()); 188 if (!visited.has(nextProgram.peer)) { 189 queue.push(nextProgram); 190 } 191 } 192 } 193 } 194 const hasLegacy = this.legacyStructMap.size ? true : false; 195 let programScript = program.astNode; 196 programScript = this.visitor(programScript, program, this.externalSourceName, hasLegacy); 197 const visitorsToReset = flattenVisitorsInHooks(this.hooks, this.state); 198 visitorsToReset.forEach((visitor) => visitor.reset()); 199 return program; 200 } 201 202 preVisitor( 203 node: arkts.AstNode, 204 program?: arkts.Program, 205 externalSourceName?: string 206 ): void { 207 const isExternal: boolean = !!externalSourceName; 208 let hook: ProgramHookLifeCycle | undefined = isExternal ? this.hooks?.external : this.hooks?.source; 209 let script: arkts.EtsScript = node as arkts.EtsScript; 210 211 const preVisitors = hook?.pre?.visitors ?? []; 212 for (const transformer of preVisitors) { 213 transformer.isExternal = isExternal; 214 transformer.externalSourceName = externalSourceName; 215 transformer.program = program; 216 transformer.visitor(script); 217 if (!this.hooks?.external?.pre?.resetAfter) { 218 transformer.reset(); 219 } 220 } 221 } 222 223 postVisitor( 224 node: arkts.AstNode, 225 program?: arkts.Program, 226 externalSourceName?: string 227 ): void { 228 const isExternal: boolean = !!externalSourceName; 229 let hook: ProgramHookLifeCycle | undefined = isExternal ? this.hooks?.external : this.hooks?.source; 230 let script: arkts.EtsScript = node as arkts.EtsScript; 231 232 const postVisitors = hook?.post?.visitors ?? []; 233 for (const transformer of postVisitors) { 234 transformer.isExternal = isExternal; 235 transformer.externalSourceName = externalSourceName; 236 transformer.program = program; 237 transformer.visitor(script); 238 if (!this.hooks?.external?.pre?.resetAfter) { 239 transformer.reset(); 240 } 241 } 242 } 243 244 visitor( 245 node: arkts.AstNode, 246 program?: arkts.Program, 247 externalSourceName?: string, 248 hasLegacy: boolean = false 249 ): arkts.EtsScript { 250 let script: arkts.EtsScript = node as arkts.EtsScript; 251 let count: number = 0; 252 const isExternal: boolean = !!externalSourceName; 253 254 // pre-run visitors 255 this.preVisitor(node, program, externalSourceName); 256 257 for (const transformer of this.visitors) { 258 transformer.isExternal = isExternal; 259 transformer.externalSourceName = externalSourceName; 260 transformer.program = program; 261 if (hasLegacy && transformer instanceof ComponentTransformer) { 262 transformer.registerMap(this.legacyStructMap); 263 } 264 script = transformer.visitor(script) as arkts.EtsScript; 265 transformer.reset(); 266 arkts.setAllParents(script); 267 if (!transformer.isExternal) { 268 debugDump( 269 script.dumpSrc(), 270 getDumpFileName(this.state, this.pluginName, count, transformer.constructor.name), 271 true, 272 this.pluginContext?.getProjectConfig()?.cachePath, 273 program!.programFileNameWithExtension 274 ); 275 count += 1; 276 } 277 } 278 279 // post-run visitors 280 this.postVisitor(node, program, externalSourceName); 281 282 return script; 283 } 284 285 visitorLegacy( 286 node: arkts.AstNode, 287 program?: arkts.Program, 288 externalSourceName?: string, 289 ): string[] { 290 const visitor = new LegacyTransformer(); 291 const script = visitor.visitor(node) as arkts.EtsScript; 292 const structList = visitor.getList(); 293 return structList; 294 } 295} 296