From 2b3a516252baecfbb15c983a28ec6fdd883da2d0 Mon Sep 17 00:00:00 2001 From: wuhailong Date: Tue, 20 May 2025 09:46:27 +0800 Subject: [PATCH 1/2] Add interop Transform in mixed compilation Issue: #ICAOTO Signed-off-by: wuhailong Change-Id: I98657ddb8f552a7dc3896db13441d8a7e9f901f6 --- .../src/fast_build/ark_compiler/error_code.ts | 3 + .../ark_compiler/module/module_mode.ts | 3 + .../ets_ui/rollup-plugin-ets-typescript.ts | 10 +- compiler/src/process_arkts_evolution.ts | 413 +++++++++++++++- .../common/process_arkts_evolution.test.ts | 468 +++++++++++++++++- 5 files changed, 893 insertions(+), 4 deletions(-) diff --git a/compiler/src/fast_build/ark_compiler/error_code.ts b/compiler/src/fast_build/ark_compiler/error_code.ts index b74ab275e..761d86a12 100644 --- a/compiler/src/fast_build/ark_compiler/error_code.ts +++ b/compiler/src/fast_build/ark_compiler/error_code.ts @@ -58,6 +58,9 @@ export enum ErrorCode { ETS2BUNDLE_EXTERNAL_ES2ABC_EXECUTION_FAILED = '10311009', ETS2BUNDLE_EXTERNAL_LAZY_IMPORT_RE_EXPORT_ERROR = '10311010', ETS2BUNDLE_EXTERNAL_DUPLICATE_FILE_NAMES_ERROR = '10311011', + // INTEROPTRANSFORMER ERROR CODE + ETS2BUNDLE_EXTERNAL_CLASS_NO_HAS_NO_ARG_CONSTRUCTOR = '10311012', + ETS2BUNDLE_EXTERNAL_UNION_TYPE_AMBIGUITY = '10311013', // CONSTANTS FOR ES2ABC ERROR CODE ES2ABC_SYNTAX_ERROR_ERROR_CODE = '10705000', diff --git a/compiler/src/fast_build/ark_compiler/module/module_mode.ts b/compiler/src/fast_build/ark_compiler/module/module_mode.ts index 01b5c1635..e058778ec 100644 --- a/compiler/src/fast_build/ark_compiler/module/module_mode.ts +++ b/compiler/src/fast_build/ark_compiler/module/module_mode.ts @@ -600,6 +600,9 @@ export class ModuleMode extends CommonMode { if (this.projectConfig.allowEtsAnnotations) { this.cmdArgs.push('--enable-annotations'); } + if (this.projectConfig.mixCompile) { + this.cmdArgs.push('--enable-ets-implements'); + } } addCacheFileArgs() { diff --git a/compiler/src/fast_build/ets_ui/rollup-plugin-ets-typescript.ts b/compiler/src/fast_build/ets_ui/rollup-plugin-ets-typescript.ts index e1c3280f9..ed58e89a7 100644 --- a/compiler/src/fast_build/ets_ui/rollup-plugin-ets-typescript.ts +++ b/compiler/src/fast_build/ets_ui/rollup-plugin-ets-typescript.ts @@ -109,6 +109,10 @@ import { MemoryDefine } from '../meomry_monitor/memory_define'; import { ModuleSourceFile } from '../ark_compiler/module/module_source_file'; import { ARKUI_SUBSYSTEM_CODE } from '../../../lib/hvigor_error_code/hvigor_error_info'; import { ProjectCollections } from 'arkguard'; +import { + interopTransformLog, + interopTransform +} from '../../process_arkts_evolution'; const filter: any = createFilter(/(? = new Map(); @@ -69,6 +83,14 @@ export let arkTSEvolutionModuleMap: Map = new Map( export let arkTSHybridModuleMap: Map = new Map(); +let arkTSEvoFileOHMUrlMap: Map = new Map(); + +let globalDeclarations: Map = new Map(); + +let declaredClassVars: Set = new Set(); + +let fullNameToTmpVar: Map = new Map(); + export function addDeclFilesConfig(filePath: string, projectConfig: Object, logger: Object, pkgPath: string, pkgName: string): void { const { projectFilePath, pkgInfo } = getPkgInfo(filePath, projectConfig, logger, pkgPath, pkgName); @@ -157,6 +179,11 @@ export function cleanUpProcessArkTSEvolutionObj(): void { arkTSEvolutionModuleMap = new Map(); arkTSHybridModuleMap = new Map(); pkgDeclFilesConfig = {}; + arkTSEvoFileOHMUrlMap = new Map(); + interopTransformLog.cleanUp(); + globalDeclarations = new Map(); + declaredClassVars = new Set(); + fullNameToTmpVar = new Map(); } export async function writeBridgeCodeFileSyncByNode(node: ts.SourceFile, moduleId: string): Promise { @@ -185,3 +212,385 @@ function getDeclgenV2OutPath(pkgName: string): string { return ''; } +export function interopTransform(program: ts.Program, id: string, mixCompile: boolean): ts.TransformerFactory { + if (!mixCompile) { + return () => sourceFile => sourceFile; + } + if (/\.ts$/.test(id)) { + return () => sourceFile => sourceFile; + } + const typeChecker: ts.TypeChecker = program.getTypeChecker(); + return (context: ts.TransformationContext): ts.Transformer => { + const scopeUsedNames: WeakMap> = new WeakMap>(); + return (rootNode: ts.SourceFile) => { + interopTransformLog.sourceFile = rootNode; + const classToInterfacesMap: Map> = collectInterfacesMap(rootNode, typeChecker); + const visitor: ts.Visitor = createObjectLiteralVisitor(context, typeChecker, scopeUsedNames); + const processNode: ts.SourceFile = ts.visitEachChild(rootNode, visitor, context); + const withHeritage: ts.SourceFile = classToInterfacesMap.size > 0 ? + ts.visitEachChild(processNode, transformHeritage(context, classToInterfacesMap), context) : processNode; + + const importStmts: ts.ImportDeclaration[] = withHeritage.statements.filter(stmt => ts.isImportDeclaration(stmt)); + const otherStmts: ts.Statement[] = withHeritage.statements.filter(stmt => !ts.isImportDeclaration(stmt)); + const globalStmts: ts.Statement[] = Array.from(globalDeclarations.values()); + + return ts.factory.updateSourceFile( + withHeritage, + [...importStmts, ...globalStmts, ...otherStmts], + withHeritage.isDeclarationFile, + withHeritage.referencedFiles, + withHeritage.typeReferenceDirectives, + withHeritage.hasNoDefaultLib, + withHeritage.libReferenceDirectives + ); + }; + }; +} + +function isFromArkTSEvolutionModule(node: ts.Node): boolean { + const sourceFile: ts.SourceFile = node.getSourceFile(); + const filePath: string = toUnixPath(sourceFile.fileName); + for (const arkTSEvolutionModuleInfo of arkTSEvolutionModuleMap.values()) { + const declgenV1OutPath: string = toUnixPath(arkTSEvolutionModuleInfo.declgenV1OutPath); + if (filePath.startsWith(declgenV1OutPath + '/')) { + const relative: string = filePath.replace(declgenV1OutPath + '/', '').replace(/\.d\.ets$/, ''); + if (!arkTSEvoFileOHMUrlMap.has(filePath)) { + arkTSEvoFileOHMUrlMap.set(filePath, relative); + } + return true; + } + } + return false; +} + +function createObjectLiteralVisitor(context: ts.TransformationContext, typeChecker: ts.TypeChecker, + scopeUsedNames: WeakMap>): ts.Visitor { + return function visitor(node: ts.SourceFile): ts.SourceFile { + if (!ts.isObjectLiteralExpression(node)) { + return ts.visitEachChild(node, visitor, context); + } + + const contextualType: ts.Type | undefined = typeChecker.getContextualType(node); + if (!contextualType) { + return ts.visitEachChild(node, visitor, context); + } + const isRecordType: boolean = contextualType.aliasSymbol?.escapedName === 'Record' && + (typeof ts.isStaticRecord === 'function' && ts.isStaticRecord(contextualType)); + const finalType: ts.Type = unwrapType(node, contextualType); + const decl : ts.Declaration = (finalType.symbol?.declarations || finalType.aliasSymbol?.declarations)?.[0]; + let className: string; + if (!isRecordType) { + if (!decl || !isFromArkTSEvolutionModule(decl)) { + return ts.visitEachChild(node, visitor, context); + } + className = finalType.symbol?.name || finalType.aliasSymbol?.name; + if (!className) { + return ts.visitEachChild(node, visitor, context); + } + + if (ts.isClassDeclaration(decl) && !hasZeroArgConstructor(decl, className)) { + return ts.visitEachChild(node, visitor, context); + } + } + + const scope: ts.Node = getScope(node); + const tmpObjName: string = getUniqueName(scope, 'tmpObj', scopeUsedNames); + const fullName: string = buildFullClassName(decl, finalType, className, isRecordType); + const getCtorExpr: ts.Expression = buildGetConstructorCall(fullName, isRecordType); + let tmpClassName: string; + if (fullNameToTmpVar.has(fullName)) { + tmpClassName = fullNameToTmpVar.get(fullName)!; + } else { + tmpClassName = getUniqueName(scope, isRecordType ? 'tmpRecord' : 'tmpClass', scopeUsedNames); + fullNameToTmpVar.set(fullName, tmpClassName); + declareGlobalTemp(tmpClassName, getCtorExpr); + } + declareGlobalTemp(tmpObjName); + + const assignments: ts.Expression[] = buildPropertyAssignments(node, tmpObjName, !isRecordType); + return ts.factory.createParenthesizedExpression(ts.factory.createCommaListExpression([ + ts.factory.createAssignment(ts.factory.createIdentifier(tmpObjName), + ts.factory.createNewExpression(ts.factory.createIdentifier(tmpClassName), undefined, [])), + ...assignments, + ts.factory.createIdentifier(tmpObjName) + ])); + }; +} + +function unwrapType(node: ts.SourceFile, type: ts.Type): ts.Type { + // Unwrap parenthesized types recursively + if ((type.flags & ts.TypeFlags.Parenthesized) && 'type' in type) { + return unwrapType(node, (type as ts.ParenthesizedType).type); + } + + // If union, pick the unique viable type + if (type.isUnion()) { + const arkTSEvolutionTypes: ts.Type[] = []; + + for (const tpye of type.types) { + const symbol: ts.Symbol = tpye.aliasSymbol || tpye.getSymbol(); + const decls: ts.Declaration[] = symbol?.declarations; + if (!decls || decls.length === 0) { + continue; + } + + const isArkTSEvolution: boolean = decls.some(decl => isFromArkTSEvolutionModule(decl)); + if (isArkTSEvolution) { + arkTSEvolutionTypes.push(tpye); + } + } + if (arkTSEvolutionTypes.length === 0) { + return type; + } + if (arkTSEvolutionTypes.length !== 1 || type.types.length > 1) { + const candidates: string = arkTSEvolutionTypes.map(tpye => tpye.symbol?.name || '(anonymous)').join(', '); + const errInfo: LogData = LogDataFactory.newInstance( + ErrorCode.ETS2BUNDLE_EXTERNAL_UNION_TYPE_AMBIGUITY, + ArkTSErrorDescription, + `Ambiguous union type: multiple valid ArkTSEvolution types found: [${candidates}].`, + '', + ['Please use type assertion as to disambiguate.'] + ); + interopTransformLog.errors.push({ + type: LogType.ERROR, + message: errInfo.toString(), + pos: node.getStart() + }); + return type; + } + return unwrapType(node, arkTSEvolutionTypes[0]); + } + return type; +} + +function hasZeroArgConstructor(decl: ts.ClassDeclaration, className: string): boolean { + const ctors = decl.members.filter(member => + ts.isConstructorDeclaration(member) || ts.isConstructSignatureDeclaration(member)); + const hasZeroArgCtor: boolean = ctors.length === 0 || ctors.some(ctor => ctor.parameters.length === 0); + if (!hasZeroArgCtor) { + const errInfo: LogData = LogDataFactory.newInstance( + ErrorCode.ETS2BUNDLE_EXTERNAL_CLASS_NO_HAS_NO_ARG_CONSTRUCTOR, + ArkTSErrorDescription, + `The class "${className}" does not has no no-argument constructor.`, + '', + [ + 'Please confirm whether there is a no-argument constructor ' + + `in the ArkTS Evolution class "${className}" type in the object literal.` + ] + ); + interopTransformLog.errors.push({ + type: LogType.ERROR, + message: errInfo.toString(), + pos: decl.name?.getStart() ?? decl.getStart() + }); + return false; + } + return hasZeroArgCtor; +} + +function buildFullClassName(decl: ts.Declaration, finalType: ts.Type, className: string, isRecordType: boolean): string { + if (isRecordType) { + return 'Lescompat/Record'; + } + const basePath: string = getArkTSEvoFileOHMUrl(finalType); + return ts.isInterfaceDeclaration(decl) + ? `L${basePath}/${className}$ObjectLiteral` + : `L${basePath}/${className}`; +} + +function buildGetConstructorCall(fullName: string, isRecord: boolean): ts.Expression { + return ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('globalThis'), 'gtest'), + isRecord ? 'etsVM.getInstance' : 'etsVM.getClass'), + undefined, + [ts.factory.createStringLiteral(fullName)] + ); +} + +function buildPropertyAssignments(node: ts.ObjectLiteralExpression, tmpObjName: string, + usePropertyAccess: boolean = true): ts.Expression[] { + return node.properties.map(property => { + if (!ts.isPropertyAssignment(property)) { + return undefined; + } + const key = property.name; + const target = usePropertyAccess && ts.isIdentifier(key) ? + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(tmpObjName), key) : + ts.factory.createElementAccessExpression(ts.factory.createIdentifier(tmpObjName), + ts.isIdentifier(key) ? ts.factory.createStringLiteral(key.text) : key); + return ts.factory.createAssignment(target, property.initializer); + }).filter(Boolean) as ts.Expression[]; +} + +function getArkTSEvoFileOHMUrl(contextualType: ts.Type): string { + const decl: ts.Declaration = (contextualType.symbol?.declarations || contextualType.aliasSymbol?.declarations)?.[0]; + if (!decl) { + return ''; + } + const sourceFilePath: string = toUnixPath(decl.getSourceFile().fileName); + return arkTSEvoFileOHMUrlMap.get(sourceFilePath); +} + +function getScope(node: ts.Node): ts.Node { + let current: ts.Node | undefined = node; + while (current && !ts.isBlock(current) && !ts.isSourceFile(current)) { + current = current.parent; + } + return current ?? node.getSourceFile(); +} + +function getUniqueName(scope: ts.Node, base: string, usedNames: WeakMap>): string { + if (!usedNames.has(scope)) { + usedNames.set(scope, new Set()); + } + const used: Set = usedNames.get(scope)!; + let name: string = base; + let counter: number = 1; + while (used.has(name)) { + name = `${base}_${counter++}`; + } + used.add(name); + return name; +} + +function declareGlobalTemp(name: string, initializer?: ts.Expression): ts.Statement { + if (initializer && declaredClassVars.has(name)) { + return globalDeclarations.get(name)!; + } + + if (!globalDeclarations.has(name)) { + const decl = ts.factory.createVariableStatement(undefined, + ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration(name, undefined, undefined, initializer)], ts.NodeFlags.Let)); + globalDeclarations.set(name, decl); + if (initializer) { + declaredClassVars.add(name); + } + } + + return globalDeclarations.get(name)!; +} + +function collectInterfacesMap(rootNode: ts.Node, checker: ts.TypeChecker): Map> { + const classToInterfacesMap = new Map>(); + ts.forEachChild(rootNode, function visit(node) { + if (ts.isClassDeclaration(node)) { + const interfaces = new Set(); + const visited = new Set(); + collectDeepInheritedInterfaces(node, checker, visited, interfaces); + if (interfaces.size > 0) { + classToInterfacesMap.set(node, interfaces); + } + } + ts.forEachChild(node, visit); + }); + return classToInterfacesMap; +} + +function collectDeepInheritedInterfaces(node: ts.ClassDeclaration | ts.InterfaceDeclaration, + checker: ts.TypeChecker, visited: Set, interfaces: Set): void { + const heritageClauses = node.heritageClauses; + if (!heritageClauses) { + return; + } + + for (const clause of heritageClauses) { + for (const exprWithTypeArgs of clause.types) { + const type = checker.getTypeAtLocation(exprWithTypeArgs.expression); + collectDeepInheritedInterfacesFromType(type, checker, visited, interfaces); + } + } +} + +function collectDeepInheritedInterfacesFromType(type: ts.Type, checker: ts.TypeChecker, + visited: Set, interfaces: Set): void { + if (visited.has(type)) { + return; + } + visited.add(type); + const decls: ts.Declaration[] = type.symbol?.declarations; + const isArkTSEvolution: boolean = decls?.some(decl => isFromArkTSEvolutionModule(decl)); + if (isArkTSEvolution) { + const ifacePath: string = getArkTSEvoFileOHMUrl(type); + interfaces.add(`L${ifacePath}/${type.symbol.name};`); + } + const baseTypes: ts.BaseType[] = checker.getBaseTypes(type as ts.InterfaceType) ?? []; + for (const baseType of baseTypes) { + collectDeepInheritedInterfacesFromType(baseType, checker, visited, interfaces); + } + + if (decls) { + for (const decl of decls) { + if (ts.isClassDeclaration(decl) || ts.isInterfaceDeclaration(decl)) { + collectDeepInheritedInterfaces(decl, checker, visited, interfaces); + } + } + } +} + +function transformHeritage(context: ts.TransformationContext, classToInterfacesMap: Map>): ts.Visitor { + return function visitor(node: ts.SourceFile): ts.SourceFile { + if (ts.isClassDeclaration(node) && classToInterfacesMap.has(node)) { + const interfaceNames = classToInterfacesMap.get(node)!; + const updatedMembers = injectImplementsInConstructor(node, interfaceNames); + return ts.factory.updateClassDeclaration(node, node.modifiers, node.name, node.typeParameters, + node.heritageClauses, updatedMembers); + } + return ts.visitEachChild(node, visitor, context); + }; +} + +function injectImplementsInConstructor(node: ts.ClassDeclaration, interfaceNames: Set): ts.ClassElement[] { + const members: ts.ClassElement[] = [...node.members]; + const params: ts.ParameterDeclaration[] = []; + const needSuper: boolean = + node.heritageClauses?.some(clause => clause.token === ts.SyntaxKind.ExtendsKeyword) || false; + const injectStatement: ts.ExpressionStatement[] = [ + ts.factory.createExpressionStatement( + ts.factory.createStringLiteral(`implement static: ${(Array.from(interfaceNames)).join(',')}`) + ) + ]; + const ctorDecl: ts.ConstructorDeclaration | undefined = + members.find(element => ts.isConstructorDeclaration(element)) as ts.ConstructorDeclaration | undefined; + if (ctorDecl) { + const newCtorDecl: ts.ConstructorDeclaration = ts.factory.updateConstructorDeclaration( + ctorDecl, ctorDecl.modifiers, ctorDecl.parameters, + ts.factory.updateBlock( + ctorDecl.body ?? ts.factory.createBlock([], true), + [...injectStatement, ...(ctorDecl.body?.statements ?? [])] + ) + ); + const index: number = members.indexOf(ctorDecl); + members.splice(index, 1, newCtorDecl); + } else { + addSuper(needSuper, injectStatement, params); + const newCtorDecl: ts.ConstructorDeclaration = ts.factory.createConstructorDeclaration( + undefined, params, + ts.factory.createBlock([...injectStatement], true) + ); + members.push(newCtorDecl); + } + return members; +} + +function addSuper(needSuper: boolean, injectStatement: ts.ExpressionStatement[], + params: ts.ParameterDeclaration[]): void { + if (needSuper) { + injectStatement.push( + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createSuper(), undefined, [ts.factory.createSpreadElement(ts.factory.createIdentifier(SUPER_ARGS))]) + ) + ); + params.push( + ts.factory.createParameterDeclaration( + undefined, + ts.factory.createToken(ts.SyntaxKind.DotDotDotToken), + ts.factory.createIdentifier(SUPER_ARGS), + undefined, + undefined, + undefined) + ); + } +} diff --git a/compiler/test/ark_compiler_ut/common/process_arkts_evolution.test.ts b/compiler/test/ark_compiler_ut/common/process_arkts_evolution.test.ts index 42a538d80..9fbaf457a 100644 --- a/compiler/test/ark_compiler_ut/common/process_arkts_evolution.test.ts +++ b/compiler/test/ark_compiler_ut/common/process_arkts_evolution.test.ts @@ -16,18 +16,373 @@ import { expect } from 'chai'; import mocha from 'mocha'; import sinon from 'sinon'; +import ts from 'typescript'; +import path from 'path'; +import proxyquire from 'proxyquire'; import { collectArkTSEvolutionModuleInfo, addDeclFilesConfig, pkgDeclFilesConfig, - arkTSModuleMap + arkTSModuleMap, + arkTSEvolutionModuleMap, + interopTransform, + interopTransformLog, + cleanUpProcessArkTSEvolutionObj } from '../../../lib/process_arkts_evolution'; import RollUpPluginMock from '../mock/rollup_mock/rollup_plugin_mock'; import { BUNDLE_NAME_DEFAULT, HAR_DECLGENV2OUTPATH } from '../mock/rollup_mock/common'; +import { + LogData, + LogDataFactory +} from '../../../lib/fast_build/ark_compiler/logger'; +import { + ArkTSErrorDescription, + ErrorCode +} from '../../../lib/fast_build/ark_compiler/error_code'; + +const testFileName: string = '/TestProject/entry/test.ets'; + +function createDualSourceProgram(testContent: string): { program: ts.Program, testSourceFile: ts.SourceFile } { + const declgenV1OutPath: string = '/TestProject/arkTSEvo/build/default/intermediates/declgen/default/declgenV1'; + arkTSEvolutionModuleMap.set('arkTSEvo', { + language:'1.2', + packageName: 'arkTSEvo', + moduleName: 'arkTSEvo', + modulePath: '/TestProject/arkTSEvo/', + declgenV1OutPath + }); + + const declFileName: string = path.join(declgenV1OutPath, 'arkTSEvo/src/main/ets/decl.d.ets'); + const declSourceFile = ts.createSourceFile(declFileName, DECLFILE_CODE, ts.ScriptTarget.ESNext, true); + const testSourceFile = ts.createSourceFile(testFileName, testContent, ts.ScriptTarget.ESNext, true); + Object.defineProperty(declSourceFile, 'fileName', { value: declFileName }); + Object.defineProperty(testSourceFile, 'fileName', { value: testFileName }); + + const compilerHost = ts.createCompilerHost({ target: ts.ScriptTarget.ESNext }); + const sourceFiles = new Map([ + [declFileName, declSourceFile], + [testFileName, testSourceFile] + ]); + compilerHost.getSourceFile = (fileName, languageVersion) => sourceFiles.get(fileName); + compilerHost.resolveModuleNames = (moduleNames, containingFile) => { + return moduleNames.map(name => { + if (name === 'arkTSEvo') { + return { + resolvedFileName: declFileName, + isExternalLibraryImport: false + }; + } + return undefined; + }); + }; + + return { + program: ts.createProgram({ rootNames: [testFileName, declFileName], options: {}, host: compilerHost }), + testSourceFile: testSourceFile + }; +} + +const DECLFILE_CODE: string = `export declare class ArkTSClass { + public get a(): number; + public set a(value: number); + constructor(); +} +export declare interface ArkTSInterface { + public get a(): number; + public set a(value: number); +} +export declare interface I1{}; +export declare interface I2{}; +export declare class ArkTSClassNoCtor { + public get a(): number; + public set a(value: number); + constructor(x: number); +} +export declare function ArkTSFuncClass(a: ArkTSClass): void; +export declare function ArkTSFuncInterface(a: ArkTSInterface): void; +export declare function ArkTSRecordFunc(rec: Record): void; +`; + +const CLASS_NO_HAS_NO_ARG_CONSTRUCTOR_CODE: string = `import { ArkTSClassNoCtor } from "arkTSEvo"; +let noCtor: ArkTSClassNoCtor = { a: 1 };`; + +const IMPORT_ARKTS_EVO_RECORD_CODE: string = `import { ArkTSRecordFunc } from "arkTSEvo"; +ArkTSRecordFunc({a: 1}); +`; + +const IMPORT_ARKTS_EVO_RECORD_CODE_EXPECT: string = `import { ArkTSRecordFunc } from "arkTSEvo"; +let tmpRecord = globalThis.gtest.etsVM.getInstance("Lescompat/Record"); +let tmpObj; +ArkTSRecordFunc((tmpObj = new tmpRecord(), tmpObj["a"] = 1, tmpObj)); +`; + +const IMPORT_ARKTS_EVO_CLASS_CODE: string = `import { ArkTSClass } from "arkTSEvo"; +let a: ArkTSClass = {a: 1}; +let an = new ArkTSClass(); +an = {a: 1}; +let aas = {a: 1} as ArkTSClass; +class A { + a: ArkTSClass = {a: 1} +} +class AA { + a: ArkTSClass; + constructor(a: ArkTSClass) { + this.a = a; + } +} +let aa = new AA({a: 1}) +class AAA { + a: ArkTSClass = new ArkTSClass(); +} +let aaa: AAA = {a: {a: 1}} +function foo(a: ArkTSClass = {a:1}) {} +function bar(a: ArkTSClass){} +bar({a: 1}) +let aaa: () => ArkTSClass = () => ({a: 1}) +function fooa(): ArkTSClass { + return {a: 1} +} +`; + +const IMPORT_ARKTS_EVO_CLASS_CODE_EXPECT: string = `import { ArkTSClass } from "arkTSEvo"; +let tmpClass = globalThis.gtest.etsVM.getClass("LarkTSEvo/src/main/ets/decl/ArkTSClass"); +let tmpObj; +let tmpObj_1; +let tmpObj_2; +let tmpObj_3; +let tmpObj_4; +let tmpObj_5; +let tmpObj_6; +let tmpObj_7; +let tmpObj_8; +let a: ArkTSClass = (tmpObj = new tmpClass(), tmpObj.a = 1, tmpObj); +let an = new ArkTSClass(); +an = (tmpObj_1 = new tmpClass(), tmpObj_1.a = 1, tmpObj_1); +let aas = (tmpObj_2 = new tmpClass(), tmpObj_2.a = 1, tmpObj_2) as ArkTSClass; +class A { + a: ArkTSClass = (tmpObj_3 = new tmpClass(), tmpObj_3.a = 1, tmpObj_3); +} +class AA { + a: ArkTSClass; + constructor(a: ArkTSClass) { + this.a = a; + } +} +let aa = new AA((tmpObj_4 = new tmpClass(), tmpObj_4.a = 1, tmpObj_4)); +class AAA { + a: ArkTSClass = new ArkTSClass(); +} +let aaa: AAA = { a: (tmpObj_5 = new tmpClass(), tmpObj_5.a = 1, tmpObj_5) }; +function foo(a: ArkTSClass = (tmpObj_6 = new tmpClass(), tmpObj_6.a = 1, tmpObj_6)) { } +function bar(a: ArkTSClass) { } +bar((tmpObj_7 = new tmpClass(), tmpObj_7.a = 1, tmpObj_7)); +let aaa: () => ArkTSClass = () => ((tmpObj_8 = new tmpClass(), tmpObj_8.a = 1, tmpObj_8)); +function fooa(): ArkTSClass { + return (tmpObj = new tmpClass(), tmpObj.a = 1, tmpObj); +} +`; + +const IMPORT_ARKTS_EVO_INTEFACE_CODE: string = `import { ArkTSInterface } from "arkTSEvo"; +let a: ArkTSInterface = {a: 1}; +let aas = {a: 1} as ArkTSInterface; +class A { + a: ArkTSInterface = {a: 1} +} +class AA { + a: ArkTSInterface; + constructor(a: ArkTSInterface) { + this.a = a; + } +} +let aa = new AA({a: 1}) +class AAA { + a: ArkTSInterface = new ArkTSClass(); +} +let aaa: AAA = {a: {a: 1}} +function foo(a: ArkTSInterface = {a:1}) {} +function bar(a: ArkTSInterface){} +bar({a: 1}) +let aaa: () => ArkTSInterface = () => ({a: 1}) +function fooa(): ArkTSInterface { + return {a: 1} +} +`; + +const IMPORT_ARKTS_EVO_INTEFACE_CODE_EXPECT: string = `import { ArkTSInterface } from "arkTSEvo"; +let tmpClass = globalThis.gtest.etsVM.getClass("LarkTSEvo/src/main/ets/decl/ArkTSInterface$ObjectLiteral"); +let tmpObj; +let tmpObj_1; +let tmpObj_2; +let tmpObj_3; +let tmpObj_4; +let tmpObj_5; +let tmpObj_6; +let tmpObj_7; +let a: ArkTSInterface = (tmpObj = new tmpClass(), tmpObj.a = 1, tmpObj); +let aas = (tmpObj_1 = new tmpClass(), tmpObj_1.a = 1, tmpObj_1) as ArkTSInterface; +class A { + a: ArkTSInterface = (tmpObj_2 = new tmpClass(), tmpObj_2.a = 1, tmpObj_2); +} +class AA { + a: ArkTSInterface; + constructor(a: ArkTSInterface) { + this.a = a; + } +} +let aa = new AA((tmpObj_3 = new tmpClass(), tmpObj_3.a = 1, tmpObj_3)); +class AAA { + a: ArkTSInterface = new ArkTSClass(); +} +let aaa: AAA = { a: (tmpObj_4 = new tmpClass(), tmpObj_4.a = 1, tmpObj_4) }; +function foo(a: ArkTSInterface = (tmpObj_5 = new tmpClass(), tmpObj_5.a = 1, tmpObj_5)) { } +function bar(a: ArkTSInterface) { } +bar((tmpObj_6 = new tmpClass(), tmpObj_6.a = 1, tmpObj_6)); +let aaa: () => ArkTSInterface = () => ((tmpObj_7 = new tmpClass(), tmpObj_7.a = 1, tmpObj_7)); +function fooa(): ArkTSInterface { + return (tmpObj = new tmpClass(), tmpObj.a = 1, tmpObj); +} +`; + +const IMPORT_ARKTS_EVO_FUNCTION_CODE: string = `import { + ArkTSFuncClass, + ArkTSFuncInterface +} from "arkTSEvo"; +ArkTSFuncClass({a: 1}); +ArkTSFuncInterface({a: 1}); +`; + +const IMPORT_ARKTS_EVO_FUNCTION_CODE_EXPECT: string = `import { ArkTSFuncClass, ArkTSFuncInterface } from "arkTSEvo"; +let tmpClass = globalThis.gtest.etsVM.getClass("LarkTSEvo/src/main/ets/decl/ArkTSClass"); +let tmpObj; +let tmpClass_1 = globalThis.gtest.etsVM.getClass("LarkTSEvo/src/main/ets/decl/ArkTSInterface$ObjectLiteral"); +let tmpObj_1; +ArkTSFuncClass((tmpObj = new tmpClass(), tmpObj.a = 1, tmpObj)); +ArkTSFuncInterface((tmpObj_1 = new tmpClass_1(), tmpObj_1.a = 1, tmpObj_1)); +`; + +const UNION_NO_AMBIGUITY_CODE: string = `import { ArkTSClass } from "arkTSEvo"; +let a: ArkTSClass | undefined = {a: 1}; +`; + +const UNION_NO_AMBIGUITY_CODE_EXPECT: string = `import { ArkTSClass } from "arkTSEvo"; +let tmpClass = globalThis.gtest.etsVM.getClass("LarkTSEvo/src/main/ets/decl/ArkTSClass"); +let tmpObj; +let a: ArkTSClass | undefined = (tmpObj = new tmpClass(), tmpObj.a = 1, tmpObj); +`; + +const UNION_AMBIGUITY_CODE: string = `import { +ArkTSClass, +ArkTSInterface +} from "arkTSEvo"; +class A { + a: number; +} +class B { + a: number; +} +let a1: A | B = {a: 1}; +let a2: ArkTSClass | ArkTSInterface = {a: 1}; +let a2: A | B | ArkTSClass | ArkTSInterface = {a: 1}; +`; + +const UNION_AMBIGUITY_CODE_EXPECT: string = `import { ArkTSClass, ArkTSInterface } from "arkTSEvo"; +class A { + a: number; +} +class B { + a: number; +} +let a1: A | B = { a: 1 }; +let a2: ArkTSClass | ArkTSInterface = { a: 1 }; +let a2: A | B | ArkTSClass | ArkTSInterface = { a: 1 }; +`; + +const PARENTHESIZED_CODE: string = `import { ArkTSClass } from "arkTSEvo"; +let a: (ArkTSClass) = {a: 1}; +`; + +const PARENTHESIZED_CODE_EXPECT: string = `import { ArkTSClass } from "arkTSEvo"; +let tmpClass = globalThis.gtest.etsVM.getClass("LarkTSEvo/src/main/ets/decl/ArkTSClass"); +let tmpObj; +let a: (ArkTSClass) = (tmpObj = new tmpClass(), tmpObj.a = 1, tmpObj); +`; + +const CLASS_IMPLEMENTS_ARKTS_EVO_CLASS_OR_INTERFACE_CODE: string = `import { I1, I2 } from "arkTSEvo"; +interface I3 {}; +class A implements I1 {} +class B extends A {} +interface I4 extends I2 {} +class C implements I3, I4 {} +class D extends B implements I4 {} +class E implements I2 { + a: number; + constructor(a: number) { + this.a = a; + } +} +class F extends D { + constructor() { + super(); + } +} +interface I5 extends I1, I4 {} +class G extends A implements I5, I3 {} +`; + +const CLASS_IMPLEMENTS_ARKTS_EVO_CLASS_OR_INTERFACE_CODE_EXPECT: string = `import { I1, I2 } from "arkTSEvo"; +interface I3 { +} +; +class A implements I1 { + constructor() { + "implement static: LarkTSEvo/src/main/ets/decl/I1;"; + } +} +class B extends A { + constructor(...args) { + "implement static: LarkTSEvo/src/main/ets/decl/I1;"; + super(...args); + } +} +interface I4 extends I2 { +} +class C implements I3, I4 { + constructor() { + "implement static: LarkTSEvo/src/main/ets/decl/I2;"; + } +} +class D extends B implements I4 { + constructor(...args) { + "implement static: LarkTSEvo/src/main/ets/decl/I1;,LarkTSEvo/src/main/ets/decl/I2;"; + super(...args); + } +} +class E implements I2 { + a: number; + constructor(a: number) { + "implement static: LarkTSEvo/src/main/ets/decl/I2;"; + this.a = a; + } +} +class F extends D { + constructor() { + "implement static: LarkTSEvo/src/main/ets/decl/I1;,LarkTSEvo/src/main/ets/decl/I2;"; + super(); + } +} +interface I5 extends I1, I4 { +} +class G extends A implements I5, I3 { + constructor(...args) { + "implement static: LarkTSEvo/src/main/ets/decl/I1;,LarkTSEvo/src/main/ets/decl/I2;"; + super(...args); + } +} +`; mocha.describe('process arkts evolution tests', function () { mocha.before(function () { @@ -115,4 +470,115 @@ mocha.describe('process arkts evolution tests', function () { expect(pkgDeclFilesConfig['har'].files['Index'].ohmUrl === expectOhmUrl).to.be.true; arkTSModuleMap.clear(); }); + + mocha.describe('3: process arkts evolution tests: interop transform', function () { + mocha.it('3-1-1: test mixCompile is false', function () { + const sourceFile: ts.SourceFile = ts.createSourceFile('a.ts', 'let x = 1;', ts.ScriptTarget.ESNext); + const program: ts.Program = ts.createProgram({ rootNames: ['a.ts'], options: {}, host: ts.createCompilerHost({}) }); + const result: ts.SourceFile = interopTransform(program, testFileName, false, false)()(sourceFile); + expect(result === sourceFile).to.be.true; + }); + + mocha.it('3-1-2: test error message of ArkTS evoluion class does not has a no-argument constructor', function () { + const { program, testSourceFile } = createDualSourceProgram(CLASS_NO_HAS_NO_ARG_CONSTRUCTOR_CODE); + const errInfo: LogData = LogDataFactory.newInstance( + ErrorCode.ETS2BUNDLE_EXTERNAL_CLASS_NO_HAS_NO_ARG_CONSTRUCTOR, + ArkTSErrorDescription, + 'The class "ArkTSClassNoCtor" does not has no no-argument constructor.', + '', + [ + 'Please confirm whether there is a no-argument constructor ' + + 'in the ArkTS Evolution class "ArkTSClassNoCtor" type in the object literal.' + ] + ); + ts.transform(testSourceFile, [interopTransform(program, testFileName, true, false)]); + const hasError = interopTransformLog.errors.some(error => + error.message.includes(errInfo.toString()) + ); + expect(hasError).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + + mocha.it('3-1-3: test import ArkTS evoluion Record in object literal', function () { + const { program, testSourceFile } = createDualSourceProgram(IMPORT_ARKTS_EVO_RECORD_CODE); + const isStaticRecord = () => (true); + const mockedTs = { + ...require('typescript'), + isStaticRecord + }; + let interopTransform; + ({ interopTransform } = proxyquire('../../../lib/process_arkts_evolution', { 'typescript': mockedTs })); + const result = ts.transform(testSourceFile, [interopTransform(program, testFileName, true, false)]).transformed[0]; + const resultCode = ts.createPrinter().printFile(result); + expect(resultCode === IMPORT_ARKTS_EVO_RECORD_CODE_EXPECT).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + + mocha.it('3-1-4: test import ArkTS evoluion class in object literal', function () { + const { program, testSourceFile } = createDualSourceProgram(IMPORT_ARKTS_EVO_CLASS_CODE); + const result = ts.transform(testSourceFile, [interopTransform(program, testFileName, true, false)]).transformed[0]; + const resultCode = ts.createPrinter().printFile(result); + expect(resultCode === IMPORT_ARKTS_EVO_CLASS_CODE_EXPECT).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + + mocha.it('3-1-5: test import ArkTS evoluion interface in object literal', function () { + const { program, testSourceFile } = createDualSourceProgram(IMPORT_ARKTS_EVO_INTEFACE_CODE); + const result = ts.transform(testSourceFile, [interopTransform(program, testFileName, true, false)]).transformed[0]; + const resultCode = ts.createPrinter().printFile(result); + expect(resultCode === IMPORT_ARKTS_EVO_INTEFACE_CODE_EXPECT).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + + mocha.it('3-1-6: test import ArkTS evoluion function in object literal', function () { + const { program, testSourceFile } = createDualSourceProgram(IMPORT_ARKTS_EVO_FUNCTION_CODE); + const result = ts.transform(testSourceFile, [interopTransform(program, testFileName, true, false)]).transformed[0]; + const resultCode = ts.createPrinter().printFile(result); + expect(resultCode === IMPORT_ARKTS_EVO_FUNCTION_CODE_EXPECT).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + + mocha.it('3-1-7: test union type (no ambiguity) in object literal', function () { + const { program, testSourceFile } = createDualSourceProgram(UNION_NO_AMBIGUITY_CODE); + const result = ts.transform(testSourceFile, [interopTransform(program, testFileName, true, false)]).transformed[0]; + const resultCode = ts.createPrinter().printFile(result); + expect(resultCode === UNION_NO_AMBIGUITY_CODE_EXPECT).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + + mocha.it('3-1-8: test union type (ambiguity) in object literal', function () { + const { program, testSourceFile } = createDualSourceProgram(UNION_AMBIGUITY_CODE); + const result = ts.transform(testSourceFile, [interopTransform(program, testFileName, true, false)]).transformed[0]; + const resultCode = ts.createPrinter().printFile(result); + const errInfo: LogData = LogDataFactory.newInstance( + ErrorCode.ETS2BUNDLE_EXTERNAL_UNION_TYPE_AMBIGUITY, + ArkTSErrorDescription, + `Ambiguous union type: multiple valid ArkTSEvolution types found: [ArkTSClass, ArkTSInterface].`, + '', + ['Please use type assertion as to disambiguate.'] + ); + const hasError = interopTransformLog.errors.some(error => + error.message.includes(errInfo.toString()) + ); + expect(hasError).to.be.true; + expect(resultCode === UNION_AMBIGUITY_CODE_EXPECT).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + + mocha.it('3-1-9: test parenthesized type in object literal', function () { + const { program, testSourceFile } = createDualSourceProgram(PARENTHESIZED_CODE); + const result = ts.transform(testSourceFile, [interopTransform(program, testFileName, true, false)]).transformed[0]; + const resultCode = ts.createPrinter().printFile(result); + expect(resultCode === PARENTHESIZED_CODE_EXPECT).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + + mocha.it('3-2-1: test class extendx arkTS evolution class or implement arkTS evolution interface', function () { + const { program, testSourceFile } = createDualSourceProgram(CLASS_IMPLEMENTS_ARKTS_EVO_CLASS_OR_INTERFACE_CODE); + const result = ts.transform(testSourceFile, [interopTransform(program, testFileName, true, true)]).transformed[0]; + const resultCode = ts.createPrinter().printFile(result); + expect(resultCode === CLASS_IMPLEMENTS_ARKTS_EVO_CLASS_OR_INTERFACE_CODE_EXPECT).to.be.true; + cleanUpProcessArkTSEvolutionObj(); + }); + }); }); \ No newline at end of file -- Gitee From 359565252b067faea7252d7fb6bcb0b943389b17 Mon Sep 17 00:00:00 2001 From: zenghang Date: Sun, 25 May 2025 19:25:36 +0800 Subject: [PATCH 2/2] Provide filemanager for interop Issue: IC99RE Signed-off-by: zenghang Change-id: ib3061b73526cca24d5dd00fae4be0974cddd47b0 --- compiler/src/ets_checker.ts | 8 +- .../ark_compiler/interop/interop_manager.ts | 332 ++++++++++++++++++ .../fast_build/ark_compiler/interop/type.ts | 69 ++++ .../ets_ui/rollup-plugin-ets-checker.ts | 2 + .../interop/interop_manager.test.ts | 197 +++++++++++ 5 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 compiler/src/fast_build/ark_compiler/interop/interop_manager.ts create mode 100644 compiler/src/fast_build/ark_compiler/interop/type.ts create mode 100644 compiler/test/ark_compiler_ut/interop/interop_manager.test.ts diff --git a/compiler/src/ets_checker.ts b/compiler/src/ets_checker.ts index e533e46d2..1ec5ea72b 100644 --- a/compiler/src/ets_checker.ts +++ b/compiler/src/ets_checker.ts @@ -113,6 +113,8 @@ import { getArkTSEvoDeclFilePath } from './process_arkts_evolution'; import { processInteropUI } from './process_interop_ui'; +import { FileManager } from './fast_build/ark_compiler/interop/interop_manager'; +import { ARKTS_1_2 } from './pre_define'; export interface LanguageServiceCache { service?: ts.LanguageService; @@ -381,7 +383,11 @@ export function createLanguageService(rootFileNames: string[], resolveModulePath // TSC will re-do resolution if this callback return true. hasInvalidatedResolutions: (filePath: string): boolean => { return reuseLanguageServiceForDepChange && needReCheckForChangedDepUsers; - } + }, + isStaticSourceFile: (fileName: string): boolean => { + const languageVersion = FileManager.getInstance().getLanguageVersionByFilePath(fileName); + return languageVersion?.languageVersion === ARKTS_1_2; + }, }; if (process.env.watchMode === 'true') { diff --git a/compiler/src/fast_build/ark_compiler/interop/interop_manager.ts b/compiler/src/fast_build/ark_compiler/interop/interop_manager.ts new file mode 100644 index 000000000..a9f8b36c0 --- /dev/null +++ b/compiler/src/fast_build/ark_compiler/interop/interop_manager.ts @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { sdkConfigs } from '../../../../main'; +import { toUnixPath } from '../../../utils'; +import { + ARKTS_1_1, + ArkTSEvolutionModule, + ARKTS_1_2, AliasConfig, + HYBRID, FileInfo +} from './type'; +import fs from 'fs'; +import path from 'path'; +import * as ts from 'typescript'; + +export class FileManager { + private static instance: FileManager | undefined = undefined; + + static arkTSModuleMap: Map = new Map(); + static aliasConfig: Map> = new Map(); + static dynamicLibPath: Set = new Set(); + static staticSDKDeclPath: Set = new Set(); + static staticSDKGlueCodePath: Set = new Set(); + static mixCompile: boolean = false; + static glueCodeFileInfos: Map = new Map(); + + private constructor() { } + + static init( + dependentModuleMap: Map, + aliasPaths: Map, + dynamicSDKPath?: Set, + staticSDKDeclPath?: Set, + staticSDKGlueCodePath?: Set + ): void { + if (FileManager.instance === undefined) { + FileManager.instance = new FileManager(); + FileManager.initLanguageVersionFromDependentModuleMap(dependentModuleMap); + FileManager.initAliasConfig(aliasPaths); + FileManager.initSDK(dynamicSDKPath, staticSDKDeclPath, staticSDKGlueCodePath); + } + } + + static getInstance(): FileManager { + if (!FileManager.instance) { + FileManager.instance = new FileManager(); + } + return FileManager.instance; + } + + private static initLanguageVersionFromDependentModuleMap( + dependentModuleMap: Map + ): void { + const convertedMap = new Map(); + + for (const [key, module] of dependentModuleMap) { + const convertedModule: ArkTSEvolutionModule = { + ...module, + modulePath: toUnixPath(module.modulePath), + declgenV1OutPath: module.declgenV1OutPath ? toUnixPath(module.declgenV1OutPath) : undefined, + declgenV2OutPath: module.declgenV2OutPath ? toUnixPath(module.declgenV2OutPath) : undefined, + declgenBridgeCodePath: module.declgenBridgeCodePath ? toUnixPath(module.declgenBridgeCodePath) : undefined, + declFilesPath: module.declFilesPath ? toUnixPath(module.declFilesPath) : undefined, + }; + convertedMap.set(key, convertedModule); + } + + this.arkTSModuleMap = convertedMap; + } + +private static initAliasConfig(aliasPaths: Map): void { + if (!aliasPaths) return; + + for (const [pkgName, filePath] of aliasPaths) { + try { + const rawContent = fs.readFileSync(filePath, 'utf-8'); + const jsonData = JSON.parse(rawContent); + const pkgAliasMap = new Map(); + + for (const [aliasKey, config] of Object.entries(jsonData)) { + if (!FileManager.isValidAliasConfig(config)) { + throw new Error(`Invalid AliasConfig format in ${pkgName} -> ${aliasKey}`); + } + + const aliasConfig = config as AliasConfig; + pkgAliasMap.set(aliasKey, { + originalAPIName: aliasConfig.originalAPIName, + isStatic: aliasConfig.isStatic + }); + } + + this.aliasConfig.set(pkgName, pkgAliasMap); + } catch (error) { + console.error(`Failed to init alias config for ${pkgName}:`, error); + const msg = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to init alias config for ${pkgName}: ${msg}`); + } + } +} + +private static isValidAliasConfig(config: unknown): config is AliasConfig { + return ( + typeof config === 'object' && + config !== null && + 'originalAPIName' in config && + typeof (config as any).originalAPIName === 'string' && + 'isStatic' in config && + typeof (config as any).isStatic === 'boolean' + ); +} + + + + private static initSDK( + dynamicSDKPath?: Set, + staticSDKBaseUrl?: Set, + staticSDKGlueCodePaths?: Set + ): void { + if (dynamicSDKPath) { + for (const path of dynamicSDKPath) { + FileManager.dynamicLibPath.add(toUnixPath(path)); + } + } + if (staticSDKBaseUrl) { + for (const path of staticSDKBaseUrl) { + FileManager.staticSDKDeclPath.add(toUnixPath(path)); + } + } + if (staticSDKGlueCodePaths) { + for (const path of staticSDKGlueCodePaths) { + FileManager.staticSDKGlueCodePath.add(toUnixPath(path)); + } + } + } + + public static cleanFileManagerObject(): void { + if (this.instance) { + this.instance.keyCache.clear(); + this.instance = undefined; + } + } + + getLanguageVersionByFilePath(filePath: string): { + languageVersion: string, + pkgName: string + } | undefined { + const path = toUnixPath(filePath); + + const moduleMatch = FileManager.matchModulePath(path); + if (moduleMatch) { + return moduleMatch; + } + + const sdkMatch = FileManager.matchSDKPath(path); + if (sdkMatch) { + return sdkMatch; + } + + return undefined; + } + + private static matchModulePath(path: string): { + languageVersion: string, + pkgName: string + } | undefined { + for (const [, moduleInfo] of FileManager.arkTSModuleMap) { + if (!path.startsWith(moduleInfo.modulePath)) { + continue; + } + + const isHybrid = moduleInfo.language === HYBRID; + const pkgName = moduleInfo.packageName; + + if (!isHybrid) { + return { + languageVersion: moduleInfo.language, + pkgName + }; + } + + const isDynamic = + moduleInfo.dynamicFiles.includes(path) || + (moduleInfo.declgenV2OutPath && path.startsWith(moduleInfo.declgenV2OutPath)); + + if (isDynamic) { + return { + languageVersion: ARKTS_1_1, + pkgName + }; + } + + const isStatic = + moduleInfo.staticFiles.includes(path) || + (moduleInfo.declgenV1OutPath && path.startsWith(moduleInfo.declgenV1OutPath)) || + (moduleInfo.declgenBridgeCodePath && path.startsWith(moduleInfo.declgenBridgeCodePath)); + + if (isStatic) { + return { + languageVersion: ARKTS_1_2, + pkgName + }; + } + } + + return undefined; + } + + private static matchSDKPath(path: string): { + languageVersion: string, + pkgName: string + } | undefined { + const sdkMatches: [Set, string][] = [ + [FileManager.dynamicLibPath, ARKTS_1_1], + [FileManager.staticSDKDeclPath, ARKTS_1_2], + [FileManager.staticSDKGlueCodePath, ARKTS_1_2], + ]; + + for (const [paths, version] of sdkMatches) { + if (paths?.some(p => path.startsWith(p))) { + return { languageVersion: version, pkgName: 'SDK' }; + } + } + + return undefined; + } + + queryOriginApiName(moduleName: string, containingFile: string): AliasConfig { + const result = this.getLanguageVersionByFilePath(containingFile); + if (!result) { + return undefined; + }; + + const alias = FileManager.aliasConfig.get(result.pkgName); + if (!alias) { + return undefined; + }; + + return alias.get(moduleName); + } + + getGlueCodePathByModuleRequest(moduleRequest: string): { + fullPath: string, + basePath: string, + } { + const extensions = ['.ts', '.ets']; + + for (const basePath of FileManager.staticSDKGlueCodePath) { + const resolvedPaths = extensions.map(ext => { + const fullPath = path.resolve(basePath, moduleRequest + ext); + return { fullPath, basePath }; + }); + + const existing = resolvedPaths.find(({ fullPath }) => fs.existsSync(fullPath)); + + if (existing) { + return { + fullPath: toUnixPath(existing.fullPath), + basePath: toUnixPath(existing.basePath), + }; + } + } + + return undefined; + } +} + +export function initFileManagerInRollup(share: Object): void { + if (!share.projectConfig.mixCompile) { + return; + } + + FileManager.mixCompile = true; + const sdkInfo = collectSDKInfo(share); + + FileManager.init( + share.projectConfig.dependentModuleMap, + share.projectConfig.aliasPaths, + sdkInfo.dynamicSDKPath, + sdkInfo.staticSDKInteropDecl, + sdkInfo.staticSDKGlueCodePath + ); +} + +function collectSDKInfo(share: Object): { + dynamicSDKPath: Set, + staticSDKInteropDecl: Set, + staticSDKGlueCodePath: Set +} { + const dynamicSDKPath: Set = new Set(); + + const staticSDKInteropDecl: Set = new Set([ + path.resolve(share.projectConfig.etsLoaderPath, '../../../ets1.2interop/declarations/kit'), + path.resolve(share.projectConfig.etsLoaderPath, '../../../ets1.2interop/declarations/api'), + path.resolve(share.projectConfig.etsLoaderPath, '../../../ets1.2interop/declarations/arkts'), + ].map(toUnixPath)); + + const staticSDKGlueCodePath: Set = new Set([ + path.resolve(share.projectConfig.etsLoaderPath, '../../../ets1.2interop/bridge/kit'), + path.resolve(share.projectConfig.etsLoaderPath, '../../../ets1.2interop/bridge/api'), + path.resolve(share.projectConfig.etsLoaderPath, '../../../ets1.2interop/bridge/arkts'), + ].map(toUnixPath)); + sdkConfigs.forEach(({ apiPath }) => { + apiPath.forEach(path => { + dynamicSDKPath.add(toUnixPath(path)); + }); + }); + + const declarationsPath: string = path.resolve(share.projectConfig.etsLoaderPath, './declarations').replace(/\\/g, '/'); + const componentPath: string = path.resolve(share.projectConfig.etsLoaderPath, './component').replace(/\\/g, '/'); + const etsComponentPath: string = path.resolve(share.projectConfig.etsLoaderPath, '../components').replace(/\\/g, '/'); + dynamicSDKPath.add(declarationsPath); + dynamicSDKPath.add(componentPath); + dynamicSDKPath.add(etsComponentPath); + + return { + dynamicSDKPath: dynamicSDKPath, + staticSDKInteropDecl: staticSDKInteropDecl, + staticSDKGlueCodePath: staticSDKGlueCodePath + }; +} \ No newline at end of file diff --git a/compiler/src/fast_build/ark_compiler/interop/type.ts b/compiler/src/fast_build/ark_compiler/interop/type.ts new file mode 100644 index 000000000..811f8e65b --- /dev/null +++ b/compiler/src/fast_build/ark_compiler/interop/type.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface ArkTSEvolutionModule { + language: string; + packageName: string; + moduleName: string; + modulePath: string; + declgenV1OutPath?: string; + declgenV2OutPath?: string; + declgenBridgeCodePath?: string; + declFilesPath?: string; + dynamicFiles: string[]; + staticFiles: string[]; + cachePath: string; + byteCodeHarInfo?: Object; +} + +export const ARKTS_1_2: string = '1.2'; +export const ARKTS_1_1: string = '1.1'; +export const ARKTS_1_0: string = '1.0'; +export const HYBRID: string = 'hybrid'; + +export interface Params { + dependentModuleMap: Map; + projectConfig: ProjectConfig; + tasks: taskInfo[]; +} + +interface ProjectConfig { + cachePath: string; + bundleName: string; + mainModuleName: string; + projectRootPath: string; +}; + +enum BuildType { + DECLGEN = 'declgen', + BYTE_CODE_HARY = 'byteCodeHar', + INTEROP_CONTEXT = 'interopContext' +} + +interface taskInfo { + packageName: string; + buildTask: BuildType +} + +export interface AliasConfig { + originalAPIName: string; + isStatic: boolean; +} + +export interface FileInfo { + recordName: string; + baseUrl: string; + abstractPath: string; +} \ No newline at end of file diff --git a/compiler/src/fast_build/ets_ui/rollup-plugin-ets-checker.ts b/compiler/src/fast_build/ets_ui/rollup-plugin-ets-checker.ts index 46aecc9e8..2838bfc2a 100644 --- a/compiler/src/fast_build/ets_ui/rollup-plugin-ets-checker.ts +++ b/compiler/src/fast_build/ets_ui/rollup-plugin-ets-checker.ts @@ -47,6 +47,7 @@ import { MemoryDefine } from '../meomry_monitor/memory_define'; import { LINTER_SUBSYSTEM_CODE } from '../../hvigor_error_code/hvigor_error_info'; import { ErrorCodeModule } from '../../hvigor_error_code/const/error_code_module'; import { collectArkTSEvolutionModuleInfo } from '../../process_arkts_evolution'; +import { initFileManagerInRollup } from '../ark_compiler/interop/interop_manager'; export let tsWatchEmitter: EventEmitter | undefined = undefined; export let tsWatchEndPromise: Promise; @@ -60,6 +61,7 @@ export function etsChecker() { if (this.share.projectConfig.dependentModuleMap) { collectArkTSEvolutionModuleInfo(this.share); } + initFileManagerInRollup(this.share); const compilationTime: CompilationTimeStatistics = new CompilationTimeStatistics(this.share, 'etsChecker', 'buildStart'); if (process.env.watchMode === 'true' && process.env.triggerTsWatch === 'true') { tsWatchEmitter = new EventEmitter(); diff --git a/compiler/test/ark_compiler_ut/interop/interop_manager.test.ts b/compiler/test/ark_compiler_ut/interop/interop_manager.test.ts new file mode 100644 index 000000000..713eeb61d --- /dev/null +++ b/compiler/test/ark_compiler_ut/interop/interop_manager.test.ts @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use rollupObject file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import mocha from 'mocha'; +import { FileManager } from '../../../lib/fast_build/ark_compiler/interop/interop_manager'; +import { ARKTS_1_1, ARKTS_1_2, HYBRID } from '../../../lib/fast_build/ark_compiler/interop/type'; + +export interface ArkTSEvolutionModule { + language: string; + packageName: string; + moduleName: string; + modulePath: string; + declgenV1OutPath?: string; + declgenV2OutPath?: string; + declgenBridgeCodePath?: string; + declFilesPath?: string; + dynamicFiles: string[]; + staticFiles: string[]; + cachePath: string; + byteCodeHarInfo?: Object; +} + +mocha.describe('test interop_manager file api', function () { + mocha.before(function () { + const dependentModuleMap: Map = new Map(); + const dynamicSDKPath: Set = new Set([ + '/sdk/default/openharmony/ets/ets1.1/api', + '/sdk/default/openharmony/ets/ets1.1/arkts', + '/sdk/default/openharmony/ets/ets1.1/kits', + '/sdk/default/openharmony/ets/ets1.1/build-tools/ets-loader/declarations', + '/sdk/default/openharmony/ets/ets1.1/build-tools/ets-loader/component', + '/sdk/default/openharmony/ets/ets1.1/build-tools/components' + ]); + const staticSDKDeclPath: Set = new Set([ + '/sdk/default/openharmony/ets/ets1.2interop/declarations/kit', + '/sdk/default/openharmony/ets/ets1.2interop/declarations/api', + '/sdk/default/openharmony/ets/ets1.2interop/declarations/arkts' + ]); + const staticSDKGlueCodePath: Set = new Set([ + '/sdk/default/openharmony/ets/ets1.2interop/bridge/kit', + '/sdk/default/openharmony/ets/ets1.2interop/bridge/api', + '/sdk/default/openharmony/ets/ets1.2interop/bridge/arkts' + ]); + dependentModuleMap.set('application', { + language: ARKTS_1_1, + packageName: 'application', + moduleName: 'application', + modulePath: '/MyApplication16/application', + declgenV1OutPath: '/MyApplication16/application/build/default/intermediates/declgen/default/declgenV1', + declgenV2OutPath: '/MyApplication16/application/build/default/intermediates/declgen/default/declgenV2', + declgenBridgeCodePath: '/MyApplication16/application/build/default/intermediates/declgen/default/declgenBridgeCode', + declFilesPath: '/MyApplication16/application/build/default/intermediates/declgen/default/decl-fileInfo.json', + dynamicFiles: [], + staticFiles: [], + cachePath: '/MyApplication16/application/build/cache', + byteCodeHarInfo: {} + }); + + dependentModuleMap.set('harv2', { + language: ARKTS_1_2, + packageName: 'harv2', + moduleName: 'harv2', + modulePath: '/MyApplication16/harv2', + declgenV1OutPath: '/MyApplication16/harv2/build/default/intermediates/declgen/default/declgenV1', + declgenV2OutPath: '/MyApplication16/harv2/build/default/intermediates/declgen/default/declgenV2', + declgenBridgeCodePath: '/MyApplication16/harv2/build/default/intermediates/declgen/default/declgenBridgeCode', + declFilesPath: '/MyApplication16/harv2/build/default/intermediates/declgen/default/decl-fileInfo.json', + dynamicFiles: [], + staticFiles: [], + cachePath: '/MyApplication16/harv2/build/cache', + byteCodeHarInfo: {} + }); + + dependentModuleMap.set('dynamic1', { + language: ARKTS_1_1, + packageName: 'dynamic1', + moduleName: 'dynamic1', + modulePath: '/MyApplication16/dynamic1', + declgenV1OutPath: '/MyApplication16/dynamic1/build/default/intermediates/declgen/default/declgenV1', + declgenV2OutPath: '/MyApplication16/dynamic1/build/default/intermediates/declgen/default/declgenV2', + declgenBridgeCodePath: '/MyApplication16/dynamic1/build/default/intermediates/declgen/default/declgenBridgeCode', + declFilesPath: '/MyApplication16/dynamic1/build/default/intermediates/declgen/default/decl-fileInfo.json', + dynamicFiles: [], + staticFiles: [], + cachePath: '/MyApplication16/dynamic1/build/cache', + byteCodeHarInfo: {} + }); + + dependentModuleMap.set('hybrid', { + language: HYBRID, + packageName: 'hybrid', + moduleName: 'hybrid', + modulePath: '/MyApplication16/hybrid', + declgenV1OutPath: '/MyApplication16/hybrid/build/default/intermediates/declgen/default/declgenV1', + declgenV2OutPath: '/MyApplication16/hybrid/build/default/intermediates/declgen/default/declgenV2', + declgenBridgeCodePath: '/MyApplication16/hybrid/build/default/intermediates/declgen/default/declgenBridgeCode', + declFilesPath: '/MyApplication16/hybrid/build/default/intermediates/declgen/default/decl-fileInfo.json', + dynamicFiles: ['/MyApplication16/hybrid/fileV1.ets'], + staticFiles: ['/MyApplication16/hybrid/fileV2.ets'], + cachePath: '/MyApplication16/hybrid/build/cache', + byteCodeHarInfo: {} + }); + FileManager.cleanFileManagerObject(); + FileManager.init( + dependentModuleMap, + new Map(), + dynamicSDKPath, + staticSDKDeclPath, + staticSDKGlueCodePath); + }); + + mocha.after(() => { + }); + + mocha.it('1-1: test SDK path', function() { + const filePath = '/sdk/default/openharmony/ets/ets1.1/api/TestAPI.ets'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_1); + expect(result?.pkgName).to.equal('SDK'); + }); + + mocha.it('1-2: test ets-loader/declarations path', function() { + const filePath = '/sdk/default/openharmony/ets/ets1.1/build-tools/ets-loader/declarations/TestAPI.ets'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_1); + expect(result?.pkgName).to.equal('SDK'); + }); + + mocha.it('1-3: test ets-loader/component path', function() { + const filePath = '/sdk/default/openharmony/ets/ets1.1/build-tools/ets-loader/component/TestAPI.d.ts'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_1); + expect(result?.pkgName).to.equal('SDK'); + }); + + mocha.it('1-4: test SDK glue code path', function() { + const filePath = '/sdk/default/openharmony/ets/ets1.2interop/bridge/arkts/TestAPI.d.ts'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_2); + expect(result?.pkgName).to.equal('SDK'); + }); + + mocha.it('1-5: test SDK interop decl path', function() { + const filePath = '/sdk/default/openharmony/ets/ets1.2interop/declarations/kit/TestAPI.d.ts'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_2); + expect(result?.pkgName).to.equal('SDK'); + }); + + mocha.it('1-6: test source code from 1.1 module', function() { + const filePath = '/MyApplication16/application/sourceCode.ets'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_1); + expect(result?.pkgName).to.equal('application'); + }); + + mocha.it('1-7: test glue code file from 1.2 module', function() { + const filePath = '/MyApplication16/harv2/build/default/intermediates/declgen/default/declgenBridgeCode/sourceCode.ets'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_2); + expect(result?.pkgName).to.equal('harv2'); + }); + + mocha.it('1-8: test decl file from 1.2 module', function() { + const filePath = '/MyApplication16/harv2/build/default/intermediates/declgen/default/declgenV1/sourceCode.ets'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_2); + expect(result?.pkgName).to.equal('harv2'); + }); + + mocha.it('1-8: test source code file from hybrid module', function() { + const filePath = '/MyApplication16/hybrid/fileV1.ets'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_1); + expect(result?.pkgName).to.equal('hybrid'); + }); + + mocha.it('1-8: test source code file from hybrid module', function() { + const filePath = '/MyApplication16/hybrid/build/default/intermediates/declgen/default/declgenV1/file1'; + const result = FileManager.getInstance().getLanguageVersionByFilePath(filePath); + expect(result?.languageVersion).to.equal(ARKTS_1_2); + expect(result?.pkgName).to.equal('hybrid'); + }); +}); \ No newline at end of file -- Gitee