• 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 ts from 'typescript';
17import * as fs from 'fs';
18import * as path from 'path';
19import { assert } from 'console';
20import uiconfig from './arkui_config_util';
21import { ComponentFile } from './component_file';
22import { analyzeBaseClasses, isComponentHerirage, getBaseClassName, removeDuplicateMethods, mergeUniqueOrdered } from './lib/attribute_utils';
23
24function readLangTemplate(): string {
25    return fs.readFileSync('./pattern/arkts_component_decl.pattern', 'utf8');
26}
27
28function extractSignatureComment(
29    signature: ts.CallSignatureDeclaration,
30    sourceFile: ts.SourceFile
31): string {
32    const jsDoc = (signature as any).jsDoc?.[0] as ts.JSDoc | undefined;
33    if (!jsDoc) return '';
34
35
36    const commentText = sourceFile.text
37        .slice(jsDoc.getStart(sourceFile), jsDoc.getEnd());
38
39    return commentText.split('\n').map((l, index) => {
40        if (index == 0) {
41            return l.trimStart();
42        }
43        return ' ' + l.trimStart();
44    }).join('\n');
45}
46
47interface ComponnetFunctionInfo {
48    sig: string[],
49    comment: string;
50}
51
52interface ComponentPram {
53    name: string,
54    type: string[],
55    isOptional: boolean,
56}
57
58function getAllInterfaceCallSignature(node: ts.InterfaceDeclaration, originalCode: ts.SourceFile, mergeCallSig: boolean = false): Array<ComponnetFunctionInfo> {
59    const signatureParams: Array<string[]> = [];
60    const comments: string[] = [];
61    const paramList: Array<ComponentPram[]> = [];
62
63    node.members.forEach(member => {
64        if (ts.isCallSignatureDeclaration(member)) {
65            const currentSignature: string[] = [];
66            const currentParam: ComponentPram[] = [];
67            const comment = extractSignatureComment(member, originalCode);
68            comments.push(comment);
69
70            member.parameters.forEach(param => {
71                currentSignature.push(param.getText(originalCode));
72                currentParam.push({ name: (param.name as ts.Identifier).escapedText as string, type: [param.type!.getText(originalCode)], isOptional: !!param.questionToken });
73            });
74            signatureParams.push(currentSignature);
75            paramList.push(currentParam);
76        }
77    });
78
79    const result: Array<ComponnetFunctionInfo> = new Array;
80
81    if (mergeCallSig) {
82        const mergedParamList: Array<ComponentPram> = [];
83        paramList.forEach((params, _) => {
84            params.forEach((param, index) => {
85                if (!mergedParamList[index]) {
86                    mergedParamList.push(param);
87                    if (index > 0) {
88                        (mergedParamList[index] as ComponentPram).isOptional = true;
89                    }
90                } else {
91                    mergedParamList[index] = { name: param.name, type: mergeUniqueOrdered(mergedParamList[index].type, param.type), isOptional: mergedParamList[index].isOptional || param.isOptional };
92                }
93            });
94        });
95        const mergedSignature: string[] = [];
96        mergedParamList.forEach((param, index) => {
97            mergedSignature.push(`${param.name}${param.isOptional ? '?' : ''}: ${param.type.join(' | ')}`);
98        });
99        result.push({
100            sig: mergedSignature,
101            comment: ''
102        });
103    } else {
104        for (let i = 0; i < signatureParams.length; i++) {
105            result.push({ sig: signatureParams[i], comment: comments[i] });
106        }
107    }
108    return result;
109}
110
111function handleComponentInterface(node: ts.InterfaceDeclaration, file: ComponentFile) {
112    const result = getAllInterfaceCallSignature(node, file.sourceFile, !uiconfig.useMemoM3);
113    const declPattern = readLangTemplate();
114    const declComponentFunction: string[] = [];
115    const attributeName = node.name!.escapedText as string;
116    const componentName = attributeName.replace(/Interface/g, '');
117    result.forEach(p => {
118        declComponentFunction.push(declPattern
119            .replace(/%COMPONENT_NAME%/g, componentName)
120            .replace(/%FUNCTION_PARAMETERS%/g, p.sig?.map(it => `${it}, `).join("") ?? "")
121            .replace(/%COMPONENT_COMMENT%/g, p.comment));
122    });
123    return declComponentFunction.join('\n');
124}
125
126function updateMethodDoc(node: ts.MethodDeclaration): ts.MethodDeclaration {
127    const returnType = ts.factory.createThisTypeNode();
128    if ('jsDoc' in node) {
129        const paramNameType: Map<string, ts.TypeNode> = new Map();
130        node.parameters.forEach(param => {
131            paramNameType.set((param.name as ts.Identifier).escapedText!, param.type!);
132        });
133        const jsDoc = node.jsDoc as ts.JSDoc[];
134        const updatedJsDoc = jsDoc.map((doc) => {
135            const updatedTags = (doc.tags || []).map((tag: ts.JSDocTag) => {
136                if (tag.tagName.escapedText === 'returns') {
137                    return ts.factory.updateJSDocReturnTag(
138                        tag as ts.JSDocReturnTag,
139                        tag.tagName,
140                        ts.factory.createJSDocTypeExpression(returnType),
141                        tag.comment
142                    );
143                }
144                if (tag.tagName.escapedText === 'param') {
145                    const paramTag = tag as ts.JSDocParameterTag;
146                    return ts.factory.updateJSDocParameterTag(
147                        paramTag,
148                        paramTag.tagName,
149                        paramTag.name,
150                        paramTag.isBracketed,
151                        ts.factory.createJSDocTypeExpression(paramNameType.get((paramTag.name as ts.Identifier).escapedText!)!),
152                        paramTag.isNameFirst,
153                        paramTag.comment
154                    );
155                }
156                return tag;
157            });
158            return ts.factory.updateJSDocComment(doc, doc.comment, updatedTags);
159        });
160        (node as any).jsDoc = updatedJsDoc;
161    }
162    return node;
163}
164
165function handleOptionalType(paramType: ts.TypeNode, wrapUndefined: boolean = true): ts.TypeNode {
166    if (!ts.isTypeReferenceNode(paramType)) {
167        return paramType;
168    }
169    const typeName = (paramType.typeName as ts.Identifier).escapedText;
170
171    const wrapUndefinedOp = (type: ts.TypeNode) => {
172        if (!wrapUndefined) {
173            return type;
174        }
175        return ts.factory.createUnionTypeNode([
176            ...(ts.isUnionTypeNode(type) ? type.types : [type]),
177            ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
178        ]);
179    };
180
181    // Check if the parameter type is Optional<XX>
182    if (typeName === 'Optional' && paramType.typeArguments?.length === 1) {
183        const innerType = paramType.typeArguments[0];
184        return wrapUndefinedOp(innerType);
185    }
186    return wrapUndefinedOp(paramType);
187}
188
189function handleAttributeMember(node: ts.MethodDeclaration): ts.MethodSignature {
190    const updatedParameters = node.parameters.map(param => {
191        const paramType = param.type;
192
193        // Ensure all other parameters are XX | undefined
194        if (paramType) {
195            if (ts.isTypeReferenceNode(paramType)) {
196                return ts.factory.updateParameterDeclaration(
197                    param,
198                    undefined,
199                    param.dotDotDotToken,
200                    param.name,
201                    param.questionToken,
202                    handleOptionalType(paramType),
203                    param.initializer
204                );
205            } else if (ts.isUnionTypeNode(paramType)) {
206                const removeOptionalTypes = paramType.types.map(type => {
207                    return handleOptionalType(type, false);
208                });
209                // Check if the union type already includes undefined
210                const hasUndefined = removeOptionalTypes.some(
211                    type => type.kind === ts.SyntaxKind.UndefinedKeyword
212                );
213
214                if (!hasUndefined) {
215                    const updatedType = ts.factory.createUnionTypeNode([
216                        ...removeOptionalTypes,
217                        ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
218                    ]);
219
220                    return ts.factory.updateParameterDeclaration(
221                        param,
222                        undefined,
223                        param.dotDotDotToken,
224                        param.name,
225                        param.questionToken,
226                        updatedType,
227                        param.initializer
228                    );
229                }
230            } else {
231                // If not a union type, add | undefined
232                const updatedType = ts.factory.createUnionTypeNode([
233                    paramType,
234                    ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
235                ]);
236
237                return ts.factory.updateParameterDeclaration(
238                    param,
239                    undefined,
240                    param.dotDotDotToken,
241                    param.name,
242                    param.questionToken,
243                    updatedType,
244                    param.initializer
245                );
246            }
247        }
248
249        return param;
250    });
251
252
253    const returnType = ts.factory.createThisTypeNode();
254    const methodSignature = ts.factory.createMethodSignature(
255        undefined,
256        node.name,
257        node.questionToken,
258        node.typeParameters,
259        updatedParameters,
260        returnType
261    );
262
263    return methodSignature;
264}
265
266function handleHeritageClause(node: ts.NodeArray<ts.HeritageClause> | undefined): ts.HeritageClause[] {
267    const heritageClauses: ts.HeritageClause[] = [];
268    if (!node) {
269        return heritageClauses;
270    }
271    node.forEach(clause => {
272        const types = clause.types.map(type => {
273            if (ts.isExpressionWithTypeArguments(type) &&
274                ts.isIdentifier(type.expression) && type.typeArguments) {
275
276                return ts.factory.updateExpressionWithTypeArguments(
277                    type,
278                    type.expression,
279                    [],
280                );
281            }
282            return type;
283        });
284        const newClause = ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, types);
285        heritageClauses.push(newClause);
286    });
287    return heritageClauses;
288}
289
290function handleAttributeModifier(node: ts.ClassDeclaration, members: ts.MethodSignature[]) {
291    if (!isComponentAttribute(node)) {
292        members.forEach(m => {
293            if ((m.name as ts.Identifier).escapedText === 'attributeModifier') {
294                members.splice(members.indexOf(m), 1);
295            }
296        });
297        return;
298    }
299    members.push(
300        ts.factory.createMethodSignature(
301            undefined,
302            ts.factory.createIdentifier("attributeModifier"),
303            undefined,
304            undefined,
305            [ts.factory.createParameterDeclaration(
306                undefined,
307                undefined,
308                ts.factory.createIdentifier("modifier"),
309                undefined,
310                ts.factory.createUnionTypeNode([
311                    ts.factory.createTypeReferenceNode(
312                        ts.factory.createIdentifier("AttributeModifier"),
313                        [ts.factory.createTypeReferenceNode(
314                            node.name!,
315                            undefined
316                        )]
317                    ),
318                    ts.factory.createTypeReferenceNode(
319                        ts.factory.createIdentifier("AttributeModifier"),
320                        [ts.factory.createTypeReferenceNode(
321                            ts.factory.createIdentifier("CommonMethod"),
322                            undefined
323                        )]
324                    ),
325                    ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword)
326                ]),
327                undefined
328            )],
329            ts.factory.createThisTypeNode()
330        )
331    );
332}
333
334function transformComponentAttribute(node: ts.ClassDeclaration): ts.Node[] {
335    const members = node.members.map(member => {
336        if (!ts.isMethodDeclaration(member)) {
337            return undefined;
338        }
339        return handleAttributeMember(member);
340    }).filter((member): member is ts.MethodSignature => member !== undefined);
341
342    const filetredMethos = removeDuplicateMethods(members);
343
344    if (uiconfig.shouldHaveAttributeModifier(node.name!.escapedText as string)) {
345        handleAttributeModifier(node, filetredMethos);
346    }
347
348    const exportModifier = ts.factory.createModifier(ts.SyntaxKind.ExportKeyword);
349    const delcareModifier = ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword);
350
351    const heritageClauses = handleHeritageClause(node.heritageClauses);
352
353    const noneUIAttribute = ts.factory.createInterfaceDeclaration(
354        [exportModifier, delcareModifier],
355        node.name as ts.Identifier,
356        [],
357        heritageClauses,
358        filetredMethos
359    );
360    return [noneUIAttribute];
361}
362
363function getLeadingSpace(line: string): string {
364    let leadingSpaces = '';
365    for (const char of line) {
366        if (char === ' ') {
367            leadingSpaces += char;
368        } else {
369            break;
370        }
371    }
372    return leadingSpaces;
373}
374
375function extractMethodName(code: string): string | undefined {
376    const match = code.match(/^\s*([^(]+)/);
377    if (!match) return undefined;
378    return match[1].trim();
379}
380
381function addAttributeMemo(node: ts.ClassDeclaration, componentFile: ComponentFile) {
382    const originalSource = componentFile.sourceFile;
383    const commentRanges = ts.getLeadingCommentRanges(originalSource.text, node.pos);
384    const classStart = commentRanges?.[0]?.pos ?? node.getStart(originalSource);
385    const classEnd = node.getEnd();
386    const originalCode = originalSource.text.substring(classStart, classEnd).split('\n');
387
388    const functionSet: Set<string> = new Set();
389    node.members.forEach(m => {
390        functionSet.add((m.name! as ts.Identifier).escapedText!);
391    });
392
393    const updatedCode: string[] = [];
394    originalCode.forEach(l => {
395        const name = extractMethodName(l);
396        if (!name) {
397            updatedCode.push(l);
398            return;
399        }
400        if (functionSet.has(name)) {
401            updatedCode.push(getLeadingSpace(l) + "@memo");
402        }
403        updatedCode.push(l);
404    });
405    const attributeName = node.name!.escapedText!;
406    const superInterface = getBaseClassName(node);
407    componentFile.appendAttribute(updatedCode.join('\n')
408        .replace(`export declare interface ${attributeName}`, `export declare interface UI${attributeName}`)
409        .replace(`extends ${superInterface}`, `extends UI${superInterface}`)
410    );
411}
412
413function isComponentAttribute(node: ts.Node) {
414    if (!(ts.isClassDeclaration(node) && node.name?.escapedText)) {
415        return false;
416    }
417    return uiconfig.isComponent(node.name.escapedText, 'Attribute');
418}
419
420function isComponentInterface(node: ts.Node) {
421    if (!(ts.isInterfaceDeclaration(node) && node.name?.escapedText)) {
422        return false;
423    }
424    return uiconfig.isComponent(node.name.escapedText, 'Interface');
425}
426
427export function addMemoTransformer(componentFile: ComponentFile): ts.TransformerFactory<ts.SourceFile> {
428    return (context) => {
429        const visit: ts.Visitor = (node) => {
430            if (isComponentHerirage(node)) {
431                addAttributeMemo(node as ts.ClassDeclaration, componentFile);
432            }
433            return ts.visitEachChild(node, visit, context);
434        };
435        return (sourceFile) => { componentFile.sourceFile = sourceFile; return ts.visitNode(sourceFile, visit); };
436    };
437}
438
439export function interfaceTransformer(program: ts.Program, componentFile: ComponentFile): ts.TransformerFactory<ts.SourceFile> {
440    return (context) => {
441        const visit: ts.Visitor = (node) => {
442            if (isComponentInterface(node)) {
443                componentFile.appendFunction(handleComponentInterface(node as ts.InterfaceDeclaration, componentFile));
444                return undefined;
445            }
446            if (isComponentHerirage(node)) {
447                return transformComponentAttribute(node as ts.ClassDeclaration);
448            }
449            return ts.visitEachChild(node, visit, context);
450        };
451
452        return (sourceFile) => ts.visitNode(sourceFile, visit);
453    };
454}
455
456export function componentInterfaceCollector(program: ts.Program, componentFile: ComponentFile): ts.TransformerFactory<ts.SourceFile> {
457    return (context) => {
458        const visit: ts.Visitor = (node) => {
459            if (isComponentAttribute(node)) {
460                const attributeName = (node as ts.ClassDeclaration).name!.escapedText as string;
461                componentFile.componentName = attributeName.replace(/Attribute/g, '');
462                const baseTypes = analyzeBaseClasses(node as ts.ClassDeclaration, componentFile.sourceFile, program);
463                uiconfig.addComponentAttributeHeritage([attributeName, ...baseTypes]);
464            }
465            return ts.visitEachChild(node, visit, context);
466        };
467
468        return (sourceFile) => ts.visitNode(sourceFile, visit);
469    };
470}