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}