• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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