diff --git a/OAT.xml b/OAT.xml index 60823ce49d78d126eff09f60448d4aa7d1e3497a..df35b64bc4871f88adf1867e0c866a0b28181ca0 100644 --- a/OAT.xml +++ b/OAT.xml @@ -27,6 +27,7 @@ + diff --git a/arkui_noninterop_plugin/.gitignore b/arkui_noninterop_plugin/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..eada254a5130310fd4a3f7acac4b3d36eecd305d --- /dev/null +++ b/arkui_noninterop_plugin/.gitignore @@ -0,0 +1,6 @@ +node_modules/* +dist/* +dist-test/* +package-lock.json +tsconfig.tsbuildinfo +ut/build/* diff --git a/arkui_noninterop_plugin/.prettierignore b/arkui_noninterop_plugin/.prettierignore new file mode 100644 index 0000000000000000000000000000000000000000..da6099ce5559e911814e6b9ed69fa78afecc567c --- /dev/null +++ b/arkui_noninterop_plugin/.prettierignore @@ -0,0 +1,25 @@ +# +# Copyright (c) 2025 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. +# + +dist/** +dist-test/** +node_modules/** +test/target/** +test/ut/** +tsconfig.json +**.json5 +**.js +**.md +**.ets diff --git a/arkui_noninterop_plugin/.prettierrc.json b/arkui_noninterop_plugin/.prettierrc.json new file mode 100644 index 0000000000000000000000000000000000000000..b7bc67defc1e53b73f89851cfed22ca9deda3b69 --- /dev/null +++ b/arkui_noninterop_plugin/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "singleQuote": true, + "arrowParens": "always", + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "trailingComma": "none" +} diff --git a/arkui_noninterop_plugin/package.json b/arkui_noninterop_plugin/package.json new file mode 100644 index 0000000000000000000000000000000000000000..f2a1affb1cfb455629ecfb35a9339ae4ca957909 --- /dev/null +++ b/arkui_noninterop_plugin/package.json @@ -0,0 +1,38 @@ +{ + "name": "arkui_noninterop_plugin", + "version": "0.1.0", + "description": "transform arkui sdk for ArkTS1.2 interop", + "keywords": [ + "interop" + ], + "scripts": { + "compile": "tsc --build --verbose tsconfig.json", + "clean": "rimraf dist tsconfig.tsbuildinfo package-lock.json", + "build": "npm run clean && npm run compile", + "format": "npx prettier --write .", + "test:compile": "tsc --build --verbose test/tsconfig.json", + "test:clean": "rimraf dist-test test/build_add_import test/build_delete_noninterop", + "test:build": "npm run test:clean && npm run test:compile", + "build:all": "npm run build && npm run test:build", + "test": "npm run build:all && mocha \"dist-test/test/test.js\" --reporter-option maxDiffSize=200000", + "sample:clean": "rimraf test/build_sample test/build_sample_result tsconfig.tsbuildinfo", + "sample": "npm run sample:clean && npm run test:build && node \"dist-test/test/testSample.js\"", + "clean:all": "npm run clean && npm run test:clean && npm run sample:clean && rimraf node_modules" + }, + "devDependencies": { + "@tsconfig/recommended": "^1.0.2", + "@types/chai": "^5.2.2", + "@types/mocha": "^10.0.10", + "@types/node": "^18.19.122", + "chai": "4.3.7", + "mocha": "10.2.0", + "prettier": "latest", + "rimraf": "^6.0.1" + }, + "dependencies": { + "commander": "^13.1.0", + "fs": "^0.0.1-security", + "path": "^0.12.7", + "typescript": "npm:ohos-typescript@4.9.5-r9" + } +} diff --git a/arkui_noninterop_plugin/src/add_import/annotation.ts b/arkui_noninterop_plugin/src/add_import/annotation.ts new file mode 100644 index 0000000000000000000000000000000000000000..68f968332ed3dcc0a06edaed839f637bed613c58 --- /dev/null +++ b/arkui_noninterop_plugin/src/add_import/annotation.ts @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +export default function writeAnnotationFile(inputPath: string, outputPath: string): void { + if (!outputPath) { + outputPath = inputPath; + } + + fs.writeFileSync(path.resolve(outputPath, ANNOTATION_FILENAME), ANNOTATION, 'utf8'); +} + +const ANNOTATION_FILENAME: string = '@ohos.arkui.GlobalAnnotation.d.ets'; + +const ANNOTATION: string = ` +@Retention({policy: "SOURCE"}) +export declare @interface State {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Prop {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Link {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Observed {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Track {}; + +@Retention({policy: "SOURCE"}) +export declare @interface ObjectLink {}; + +@Retention({policy: "SOURCE"}) +export declare @interface StorageProp { + property: string; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface StorageLink { + property: string; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface LocalStorageProp { + property: string; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface LocalStorageLink { + property: string; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface Provide { + alias: string = ""; + allowOverride: boolean = false; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface Consume { + alias: string = ""; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface Watch { + callback: string; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface Require {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Local {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Param {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Once {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Event {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Provider { + alias: string = ""; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface Consumer { + alias: string = ""; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface Monitor { + path: string[]; +}; + +@Retention({policy: "SOURCE"}) +export declare @interface Computed {}; + +@Retention({policy: "SOURCE"}) +export declare @interface ObservedV2 {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Trace {}; + +@Retention({policy: "SOURCE"}) +export declare @interface Builder {} + +@Retention({policy: "SOURCE"}) +export declare @interface BuilderParam {} + +@Retention({policy: "SOURCE"}) +export declare @interface AnimatableExtend {} + +@Retention({policy: "SOURCE"}) +export declare @interface Styles {} + +@Retention({policy: "SOURCE"}) +export declare @interface Extend { + extend: Any +} + +@Retention({policy: "SOURCE"}) +export declare @interface Type { + type: Any +} + +@Retention({policy: "SOURCE"}) +export @interface Reusable {} + +@Retention({policy: "SOURCE"}) +export @interface ReusableV2 {} + +@Retention({policy: "SOURCE"}) +export @interface Entry { + routeName: string = ""; + storage: string = ""; + useSharedStorage: boolean = false; +} + +@Retention({policy: "SOURCE"}) +export @interface Component {} + +@Retention({policy: "SOURCE"}) +export @interface ComponentV2 {} + +@Retention({policy: "SOURCE"}) +export @interface CustomDialog {} +`; diff --git a/arkui_noninterop_plugin/src/add_import/handle_ui_imports.ts b/arkui_noninterop_plugin/src/add_import/handle_ui_imports.ts new file mode 100644 index 0000000000000000000000000000000000000000..739931dee81ffca974d9b3cd297048b2e21512f4 --- /dev/null +++ b/arkui_noninterop_plugin/src/add_import/handle_ui_imports.ts @@ -0,0 +1,683 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +import * as ts from 'typescript'; + +import { + API_PATH, + ARKUI_BUILDER, + ARKUI_EXTEND, + ARKUI_STYLES, + COMPONENT, + DEFAULT, + EXTNAME_D_ETS, + EXTNAME_D_TS, + GENERIC_T, + NODE_MODULES, + OHOS_ARKUI, + OHOS_ARKUI_COMPONENT, + OHOS_ARKUI_GLOBAL_ESVALUE, + OHOS_ARKUI_STATEMANAGEMENT, + OHOS_KIT_ARKUI, +} from './pre_define'; +import { + apiDir, + apiInternalDir, + whiteList, + decoratorsWhiteList, + whiteFileList, +} from './white_management'; + +export default class HandleUIImports { + private readonly typeChecker: ts.TypeChecker; + private readonly context: ts.TransformationContext; + private readonly outPath: string; + private readonly inputDir: string; + private readonly exportFlag: boolean; + private readonly printer: ts.Printer; + + private importedInterfaces: Set; + private interfacesNeedToImport: Set; + private readonly trueSymbolAtLocationCache: Map; + + private dynamicImportCollection: Map>; + private dynamicImportType: Map; + + private insertPosition: number; + + constructor(program: ts.Program, context: ts.TransformationContext, outPath: string, + inputDir: string, exportFlag: boolean) { + this.typeChecker = program.getTypeChecker(); + this.context = context; + this.outPath = outPath; + this.inputDir = inputDir; + this.exportFlag = exportFlag; + this.printer = ts.createPrinter(); + + this.importedInterfaces = new Set(); + this.interfacesNeedToImport = new Set(); + this.trueSymbolAtLocationCache = new Map(); + + this.dynamicImportCollection = new Map(); + this.dynamicImportType = new Map(); + + this.insertPosition = 0; + } + + createCustomTransformer(sourceFile: ts.SourceFile): ts.SourceFile { + if (sourceFile?.fileName) { + const name = path.basename(sourceFile.fileName, path.extname(sourceFile.fileName)); + if (name.includes(OHOS_ARKUI_GLOBAL_ESVALUE)) { + if (this.outPath) { + this.writeSourceFileToOutPut(sourceFile); + } + if (this.exportFlag) { + return ts.visitNode(sourceFile, this.visitGlobalESValueNode.bind(this)); + } + return sourceFile; + } + } + + this.extractImportedNames(sourceFile); + + const statements = sourceFile.statements; + for (let i = 0; i < statements.length; ++i) { + const statement = statements[i]; + if (!ts.isJSDoc(statement) && !(ts.isExpressionStatement(statement) && + ts.isStringLiteral(statement.expression))) { + this.insertPosition = i; + break; + } + } + + return ts.visitNode(sourceFile, this.visitNode.bind(this)); + } + + visitGlobalESValueNode(node: ts.Node): ts.Node | undefined { + if (ts.isTypeAliasDeclaration(node) && ts.isImportTypeNode(node.type)) { + return this.processDynamicImportInType(node); + } + + if (ts.isImportTypeNode(node)) { + return this.processDynamicImportInImportTypeNode(node); + } + + const result = ts.visitEachChild(node, this.visitGlobalESValueNode.bind(this), this.context); + + if (ts.isSourceFile(result)) { + this.processSourceFileForDynamicImport(node as ts.SourceFile, result); + } + + return result; + } + + isStructDeclaration(node: ts.Node): boolean { + // @ts-ignore + return ts.isStructDeclaration(node); + } + + visitNode(node: ts.Node): ts.Node | undefined { + // @ts-ignore + if (node.parent && this.isStructDeclaration(node.parent) && ts.isConstructorDeclaration(node)) { + return undefined; + } + + if (ts.isImportDeclaration(node)) { + const moduleSpecifier = node.moduleSpecifier; + if (ts.isStringLiteral(moduleSpecifier)) { + const modulePath = moduleSpecifier.text; + if ([OHOS_ARKUI_STATEMANAGEMENT, OHOS_ARKUI_COMPONENT].includes(modulePath)) { + return node; + } else if (modulePath.includes(COMPONENT + '/')) { + return this.updateImportComponentPath(node, modulePath); + } + } + } + + this.handleImportBuilder(node); + const result = ts.visitEachChild(node, this.visitNode.bind(this), this.context); + + if (ts.isIdentifier(result) && !this.shouldSkipIdentifier(result)) { + this.interfacesNeedToImport.add(result.text); + } else if (ts.isSourceFile(result)) { + this.addUIImports(result); + } + + return result; + } + + updateImportComponentPath(node: ts.ImportDeclaration, modulePath: string): ts.ImportDeclaration { + return ts.factory.updateImportDeclaration( + node, + node.modifiers, + node.importClause, + ts.factory.createStringLiteral( + modulePath.replace(/\.\.\/component\/.*/, OHOS_ARKUI_COMPONENT) + ), + node.assertClause + ); + } + + processTypeWithLongMemberChain(node: ts.TypeAliasDeclaration, moduleName: string, + memberChain: string[]): ts.TypeAliasDeclaration { + return ts.factory.updateTypeAliasDeclaration(node, + node.modifiers, + node.name, + node.typeParameters, + ts.factory.createTypeReferenceNode( + ts.factory.createQualifiedName( + ts.factory.createIdentifier(moduleName), + ts.factory.createIdentifier(memberChain[1]) + ), + undefined + ) + ); + } + + getModulePath(node: ts.ImportTypeNode): string { + return ((node.argument as unknown as ts.LiteralTypeNode) + .literal as unknown as ts.StringLiteral).text; + } + + processDynamicImportInType(node: ts.TypeAliasDeclaration): ts.Node { + const typeName = node.name.text; + const importType = node.type as ts.ImportTypeNode; + const modulePath = this.getModulePath(importType); + const moduleName = this.extractLastSegment(modulePath); + const memberChain = this.extractFromImportTypeNode(importType); + const hasGeneric = !!node.typeParameters?.length; + + if (!this.hasDefaultInMemberChain(memberChain) && + (!hasGeneric || !node.typeParameters[0].default)) { + + if (memberChain.length > 1) { + return this.processTypeWithLongMemberChain(node, moduleName, memberChain); + } + this.collectDynamicImport(modulePath, typeName); + return this.processTypeWithoutDefaultOnly(typeName, modulePath); + } + + if (this.hasDefaultInMemberChain(memberChain) && memberChain.length > 1) { + this.collectDynamicImport(modulePath, moduleName); + return this.processTypeWithDefaultAndLongMemberChain(node, modulePath, moduleName, memberChain); + } + + if (this.hasDefaultInMemberChain(memberChain) && memberChain.length === 1) { + this.collectDynamicImport(modulePath, typeName); + return this.processTypeWithDefaultAndOneMember(typeName, modulePath); + } + + if (hasGeneric && node.typeParameters[0].default) { + this.collectDynamicImport(modulePath, typeName); + return this.processHasGeneric(node, typeName, modulePath); + } + + return node; + } + + processDynamicImportInImportTypeNode(node: ts.ImportTypeNode): ts.Node { + const modulePath = this.getModulePath(node); + const moduleName = this.extractLastSegment(modulePath); + const memberChain = this.extractFromImportTypeNode(node); + + if (memberChain.includes(DEFAULT)) { + this.setImportType(modulePath, ImportType.DEFAULT); + this.collectDynamicImport(modulePath, moduleName); + return ts.factory.createIdentifier(moduleName); + } else if (memberChain.length) { + this.setImportType(modulePath, ImportType.NAMED); + this.collectDynamicImport(modulePath, memberChain[0]); + return ts.factory.createTypeReferenceNode( + node.qualifier as ts.Identifier, + node.typeArguments + ); + } + return node; + } + + processSourceFileForDynamicImport(node: ts.SourceFile, result: ts.SourceFile): void { + const newStatements = [...result.statements]; + if (this.dynamicImportCollection.size) { + this.addImportForDynamicImport(newStatements); + } + + const updatedStatements = ts.factory.createNodeArray(newStatements); + const updatedSourceFile = ts.factory.updateSourceFile(node, + updatedStatements, + node.isDeclarationFile, + node.referencedFiles, + node.typeReferenceDirectives, + node.hasNoDefaultLib, + node.libReferenceDirectives + ); + + const updatedCode = this.printer.printFile(updatedSourceFile); + if (this.outPath) { + this.writeSourceFileToOutPut(updatedSourceFile, updatedCode); + } else { + fs.writeFileSync(updatedSourceFile.fileName, updatedCode); + } + } + + addUIImports(node: ts.SourceFile): void { + const newStatements = [...node.statements]; + + const compImportSpecifiers: ts.ImportSpecifier[] = []; + const stateImportSpecifiers: ts.ImportSpecifier[] = []; + + this.interfacesNeedToImport.forEach((interfaceName) => { + if (this.importedInterfaces.has(interfaceName)) { + return; + } + const identifier = ts.factory.createIdentifier(interfaceName); + if (decoratorsWhiteList.includes(interfaceName)) { + stateImportSpecifiers.push(ts.factory.createImportSpecifier(false, undefined, identifier)); + } else { + compImportSpecifiers.push(ts.factory.createImportSpecifier(false, undefined, identifier)); + } + }); + + if (compImportSpecifiers.length + stateImportSpecifiers.length > 0) { + this.processAddUIImport(node, compImportSpecifiers, stateImportSpecifiers, newStatements); + } + + this.processSourceFileForUIImport(node, newStatements); + } + + processTypeWithoutDefaultOnly(typeName: string, modulePath: string): ts.ExportDeclaration { + this.setImportType(modulePath, ImportType.NAMED); + return ts.factory.createExportDeclaration( + undefined, + false, + ts.factory.createNamedExports([ts.factory.createExportSpecifier( + false, + undefined, + ts.factory.createIdentifier(typeName) + )]), + undefined, + undefined + ); + } + + processTypeWithDefaultAndLongMemberChain(node: ts.TypeAliasDeclaration, modulePath: string, + moduleName: string, memberChain: string[]): ts.TypeAliasDeclaration { + this.setImportType(modulePath, ImportType.DEFAULT); + return ts.factory.updateTypeAliasDeclaration(node, + node.modifiers, + node.name, + node.typeParameters, + ts.factory.createTypeReferenceNode( + ts.factory.createQualifiedName( + ts.factory.createIdentifier(moduleName), + ts.factory.createIdentifier(memberChain[1]) + ), + undefined + ) + ); + } + + processTypeWithDefaultAndOneMember(typeName: string, modulePath: string): ts.ExportDeclaration { + this.setImportType(modulePath, ImportType.DEFAULT); + return ts.factory.createExportDeclaration( + undefined, + false, + ts.factory.createNamedExports([ts.factory.createExportSpecifier( + false, + undefined, + ts.factory.createIdentifier(typeName) + )]), + undefined, + undefined + ); + } + + processHasGeneric(node: ts.TypeAliasDeclaration, typeName: string, + modulePath: string): ts.TypeAliasDeclaration { + this.setImportType(modulePath, ImportType.ALIAS); + return ts.factory.updateTypeAliasDeclaration(node, + node.modifiers, + node.name, + node.typeParameters, + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier(typeName + GENERIC_T), + [ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier(GENERIC_T), + undefined + )] + ) + ); + } + + addImportForDynamicImport(newStatements: ts.Statement[]): void { + this.dynamicImportCollection.forEach((value, key) => { + if (this.dynamicImportType.get(key) === ImportType.DEFAULT) { + newStatements.splice(1, 0, ts.factory.createImportDeclaration(undefined, + ts.factory.createImportClause(false, + ts.factory.createIdentifier(Array.from(value)[0]), undefined), + ts.factory.createStringLiteral(key), + undefined + )); + } else if (this.dynamicImportType.get(key) === ImportType.NAMED) { + const namedImports = ts.factory.createNamedImports(Array.from(value).map(v => { + return ts.factory.createImportSpecifier(false, undefined, + ts.factory.createIdentifier(v)); + })); + newStatements.splice(1, 0, ts.factory.createImportDeclaration(undefined, + ts.factory.createImportClause(false, undefined, namedImports), + ts.factory.createStringLiteral(key), undefined + )); + } else { + newStatements.splice(1, 0, ts.factory.createImportDeclaration(undefined, + ts.factory.createImportClause(false, undefined, + ts.factory.createNamedImports([ts.factory.createImportSpecifier(false, + ts.factory.createIdentifier(Array.from(value)[0]), + ts.factory.createIdentifier(Array.from(value)[0] + 'T') + )]) + ), + ts.factory.createStringLiteral(key), undefined + )); + } + }); + + this.createPromptActionDefaultImport(newStatements); + } + + createPromptActionDefaultImport(newStatements: ts.Statement[]): void { + newStatements.splice(1, 0, ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + ts.factory.createIdentifier('promptAction'), + undefined + ), + ts.factory.createStringLiteral('./@ohos.promptAction'), + undefined + )); + } + + extractFromImportTypeNode(importTypeNode: ts.ImportTypeNode): string[] { + if (!importTypeNode.qualifier) { + return []; + } + + return importTypeNode.qualifier.getText().split('.'); + } + + collectDynamicImport(k: string, v: string): void { + if (this.dynamicImportCollection.has(k)) { + this.dynamicImportCollection.get(k)!.add(v); + } else { + this.dynamicImportCollection.set(k, new Set([v])); + } + } + + extractLastSegment(path: string): string { + const slashIndex = path.lastIndexOf('/'); + const dotIndex = path.lastIndexOf('.'); + + const lastSeparatorIndex = Math.max(slashIndex, dotIndex); + if (lastSeparatorIndex !== -1 && lastSeparatorIndex < path.length - 1) { + return path.slice(lastSeparatorIndex + 1); + } + + return ''; + } + + hasDefaultInMemberChain(memberChain: string[]): boolean { + return memberChain.includes(DEFAULT); + } + + setImportType(modulePath: string, type: ImportType): void { + this.dynamicImportType.set(modulePath, type); + } + + handleImportBuilder(node: ts.Node): void { + ts.getDecorators(node as ts.HasDecorators)?.forEach(element => { + if (element?.getText() === '@' + ARKUI_BUILDER) { + this.interfacesNeedToImport.add(ARKUI_BUILDER); + return; + } + }); + } + + hasKitArkUI(node: ts.SourceFile): boolean { + return node.text?.includes(OHOS_KIT_ARKUI); + } + + getCoreFilename(fileName: string): string { + if (fileName.endsWith(EXTNAME_D_ETS)) { + return fileName.slice(0, -EXTNAME_D_ETS.length); + } + if (fileName.endsWith(EXTNAME_D_TS)) { + return fileName.slice(0, -EXTNAME_D_TS.length); + } + return fileName; + } + + isNeedAddImports(node: ts.SourceFile): boolean { + if (!ts.isSourceFile(node)) { + return false; + } + + if (node.fileName.includes(OHOS_ARKUI) || + whiteFileList.includes(this.getCoreFilename(path.basename(node.fileName))) || + this.hasKitArkUI(node)) { + return true; + } + + return false; + } + + processSourceFileForUIImport(node: ts.SourceFile, newStatements: ts.Statement[]): void { + const updatedStatements = ts.factory.createNodeArray(newStatements); + const updatedSourceFile = ts.factory.updateSourceFile(node, + updatedStatements, + node.isDeclarationFile, + node.referencedFiles, + node.typeReferenceDirectives, + node.hasNoDefaultLib, + node.libReferenceDirectives + ); + + const updatedCode = this.printer.printFile(updatedSourceFile); + if (this.outPath) { + this.writeSourceFileToOutPut(updatedSourceFile, updatedCode); + } else { + fs.writeFileSync(updatedSourceFile.fileName, updatedCode); + } + } + + writeSourceFileToOutPut(sourceFile: ts.SourceFile, context: string = sourceFile.text): void { + const outFile: string = path.resolve(sourceFile.fileName.replace(this.inputDir, + this.outPath)); + this.safeWriteFileSync(outFile, context); + } + + safeWriteFileSync(filePath: string, content: string): void { + const dir: string = path.dirname(filePath); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(filePath, content); + } + + addArkUIPath(node: ts.SourceFile, moduleName: string): string { + if (ts.isSourceFile(node)) { + const fileName = node.fileName; + if (apiDir.some(path => fileName.includes(API_PATH + path + '/'))) { + return '.' + moduleName; + } else if (apiInternalDir.some(path => fileName.includes(API_PATH + path + '/'))) { + return '../.' + moduleName; + } + } + return moduleName; + } + + processAddUIImport(node: ts.SourceFile, compImportSpecifiers: ts.ImportSpecifier[], + stateImportSpecifiers: ts.ImportSpecifier[], newStatements: ts.Statement[]): void { + if (!this.isNeedAddImports(node)) { + return; + } + + if (compImportSpecifiers.length) { + const moduleName = this.addArkUIPath(node, OHOS_ARKUI_COMPONENT); + const compImportDeclaration = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause(false, + undefined, + ts.factory.createNamedImports( + compImportSpecifiers + ) + ), + ts.factory.createStringLiteral(moduleName, true), + undefined + ); + newStatements.splice(this.insertPosition, 0, compImportDeclaration); + } + + if (stateImportSpecifiers.length) { + const moduleName = this.addArkUIPath(node, OHOS_ARKUI_STATEMANAGEMENT); + const stateImportDeclaration = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause(false, + undefined, + ts.factory.createNamedImports( + stateImportSpecifiers + ) + ), + ts.factory.createStringLiteral(moduleName, true), + undefined + ); + newStatements.splice(this.insertPosition, 0, stateImportDeclaration); + } + } + + getDeclarationNode(node: ts.Node): ts.Declaration | undefined { + const symbol = this.trueSymbolAtLocation(node); + return HandleUIImports.getDeclaration(symbol); + } + + static getDeclaration(tsSymbol: ts.Symbol | undefined): ts.Declaration | undefined { + if (tsSymbol?.declarations && tsSymbol.declarations.length > 0) { + return tsSymbol.declarations[0]; + } + + return undefined; + } + + followIfAliased(symbol: ts.Symbol): ts.Symbol { + if ((symbol.getFlags() & ts.SymbolFlags.Alias) !== 0) { + return this.typeChecker.getAliasedSymbol(symbol); + } + + return symbol; + } + + trueSymbolAtLocation(node: ts.Node): ts.Symbol | undefined { + const cache = this.trueSymbolAtLocationCache; + const val = cache.get(node); + + if (val !== undefined) { + return val !== null ? val : undefined; + } + + let symbol = this.typeChecker.getSymbolAtLocation(node); + + if (symbol === undefined) { + cache.set(node, null); + return undefined; + } + + symbol = this.followIfAliased(symbol); + cache.set(node, symbol); + + return symbol; + } + + shouldSkipIdentifier(identifier: ts.Identifier): boolean { + const name = identifier.text; + const skippedList = new Set([ARKUI_EXTEND, ARKUI_STYLES]); + if (skippedList.has(name)) { + return true; + } + + if (!whiteList.has(name)) { + return true; + } + + const symbol = this.typeChecker.getSymbolAtLocation(identifier); + if (symbol) { + const decl = this.getDeclarationNode(identifier); + if (this.isDeclFromSDK(decl, identifier)) { + return true; + } + } + + return this.interfacesNeedToImport.has(name); + } + + isDeclFromSDK(decl: ts.Declaration | undefined, identifier: ts.Identifier): boolean { + const rootNode = decl?.getSourceFile(); + if (!rootNode) { + return false; + } + + if (rootNode === identifier.getSourceFile()) { + return true; + } + + const fileName = rootNode.fileName; + if (!fileName.includes(NODE_MODULES) && fileName.includes(this.inputDir)) { + return true; + } + + return false; + } + + extractImportedNames(sourceFile: ts.SourceFile): void { + for (const statement of sourceFile.statements) { + if (!ts.isImportDeclaration(statement)) { + continue; + } + + const importClause = statement.importClause; + if (!importClause) { + continue; + } + + const namedBindings = importClause.namedBindings; + if (!namedBindings || !ts.isNamedImports(namedBindings)) { + continue; + } + + for (const specifier of namedBindings.elements) { + const importedName = specifier.name.getText(sourceFile); + this.importedInterfaces.add(importedName); + } + } + } +} + +enum ImportType { + DEFAULT = 0, + NAMED = 1, + ALIAS = 2, + DEFAULT_AND_NAMED = 3, +} diff --git a/arkui_noninterop_plugin/src/add_import/index.ts b/arkui_noninterop_plugin/src/add_import/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea0f9905dab4f47b514473ce4ae7f7563edd9bcb --- /dev/null +++ b/arkui_noninterop_plugin/src/add_import/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 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 start from './start'; + +start(); diff --git a/arkui_noninterop_plugin/src/add_import/pre_define.ts b/arkui_noninterop_plugin/src/add_import/pre_define.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee1f1e9ecf60b59db7df1aba9943584064a300de --- /dev/null +++ b/arkui_noninterop_plugin/src/add_import/pre_define.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 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 const API_PATH = '/api/'; +export const ARKUI_BUILDER = 'Builder'; +export const ARKUI_EXTEND = 'Extend'; +export const ARKUI_STYLES = 'Styles'; +export const COMPONENT = 'component'; +export const DEFAULT = 'default'; +export const EXTNAME_D_ETS = '.d.ets'; +export const EXTNAME_D_TS = '.d.ts'; +export const GENERIC_T = 'T'; +export const NODE_MODULES = 'node_modules'; +export const OHOS_ARKUI = '@ohos.arkui.'; +export const OHOS_ARKUI_COMPONENT = './@ohos.arkui.GlobalESValue'; +export const OHOS_ARKUI_GLOBAL_ESVALUE = '@ohos.arkui.GlobalESValue'; +export const OHOS_ARKUI_STATEMANAGEMENT = './@ohos.arkui.GlobalESValue'; +export const OHOS_KIT_ARKUI = '@kit ArkUI'; diff --git a/arkui_noninterop_plugin/src/add_import/process_interop_ui.ts b/arkui_noninterop_plugin/src/add_import/process_interop_ui.ts new file mode 100644 index 0000000000000000000000000000000000000000..44d06b29da8d561aab851d5b3b3dc87b3d271dbf --- /dev/null +++ b/arkui_noninterop_plugin/src/add_import/process_interop_ui.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +import * as ts from 'typescript'; + +import HandleUIImports from './handle_ui_imports'; +import writeAnnotationFile from './annotation'; +import { EXTNAME_D_ETS, EXTNAME_D_TS } from './pre_define'; + +export default function processInteropUI(inputPath: string, exportFlag: boolean, + outputPath = ''): ts.TransformationResult { + const filePaths = getDeclgenFiles(inputPath); + + const program = ts.createProgram(filePaths, defaultCompilerOptions()); + const sourceFiles = getSourceFiles(program, filePaths); + + const createTransformer = (ctx: ts.TransformationContext) => { + return (sourceFile: ts.SourceFile) => { + const handleUIImports = new HandleUIImports(program, ctx, outputPath, inputPath, exportFlag); + return handleUIImports.createCustomTransformer(sourceFile); + }; + }; + const res = ts.transform(sourceFiles, [createTransformer]); + + writeAnnotationFile(inputPath, outputPath); + return res; +} + +function getDeclgenFiles(dir: string, filePaths: string[] = []): string[] { + const files = fs.readdirSync(dir); + + files.forEach(file => { + const filePath: string = path.join(dir, file); + const stat: fs.Stats = fs.statSync(filePath); + + if (stat.isDirectory()) { + getDeclgenFiles(filePath, filePaths); + } else if (stat.isFile() && (file.endsWith(EXTNAME_D_ETS) || file.endsWith(EXTNAME_D_TS))) { + filePaths.push(filePath); + } + }); + + return filePaths; +} + +function defaultCompilerOptions(): ts.CompilerOptions { + return { + target: ts.ScriptTarget.Latest, + module: ts.ModuleKind.CommonJS, + allowJs: true, + checkJs: true, + declaration: true, + emitDeclarationOnly: true, + noEmit: false + }; +} + +function getSourceFiles(program: ts.Program, filePaths: string[]): ts.SourceFile[] { + const sourceFiles: ts.SourceFile[] = []; + + filePaths.forEach(filePath => { + sourceFiles.push(program.getSourceFile(filePath)!); + }); + + return sourceFiles; +} diff --git a/arkui_noninterop_plugin/src/add_import/start.ts b/arkui_noninterop_plugin/src/add_import/start.ts new file mode 100644 index 0000000000000000000000000000000000000000..884e6bd3e22c7a4f683aa0b3615349ab36a87101 --- /dev/null +++ b/arkui_noninterop_plugin/src/add_import/start.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 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 * as commander from 'commander'; + +import processInteropUI from './process_interop_ui'; + +let outputPath = ''; +let inputDir = ''; +let exportFlag = false; + +export default function start(): void { + const program = new commander.Command(); + program + .name('noninterop_global_import') + .version('0.0.1'); + program + .option('--input ', 'input path') + .option('--output ', 'output path') + .option('--export ', 'export flag', false) + .action((opts) => { + outputPath = opts.output; + inputDir = opts.input; + exportFlag = opts.export === 'true'; + processInteropUI(inputDir, exportFlag); + }); + program.parse(process.argv); +} \ No newline at end of file diff --git a/arkui_noninterop_plugin/src/add_import/white_management.ts b/arkui_noninterop_plugin/src/add_import/white_management.ts new file mode 100644 index 0000000000000000000000000000000000000000..80ee7429ebf474ea17da51a093a92b0d1fd79aed --- /dev/null +++ b/arkui_noninterop_plugin/src/add_import/white_management.ts @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2025 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. + */ + +const whiteList = new Set([ + 'ASTCResource', 'AbstractProperty', 'AccelerationOptions', 'AccessibilityAction', + 'AccessibilityActionInterceptResult', 'AccessibilityCallback', 'AccessibilityHoverEvent', + 'AccessibilityHoverType', 'AccessibilityOptions', 'AccessibilityRoleType', + 'AccessibilitySamePageMode', 'ActionSheet', 'ActionSheetButtonOptions', 'ActionSheetOffset', + 'ActionSheetOptions', 'AdaptiveColor', 'AdsBlockedDetails', 'Affinity', 'AlertDialog', + 'AlertDialogButtonBaseOptions', 'AlertDialogButtonOptions', 'AlertDialogParam', + 'AlertDialogParamWithButtons', 'AlertDialogParamWithConfirm', 'AlertDialogParamWithOptions', + 'AlignRuleOption', 'Alignment', 'AlphabetIndexerOptions', 'AnchoredColorMode', + 'AnimatableArithmetic', 'AnimatableExtend', 'AnimateParam', 'AnimationExtender', + 'AnimationMode', 'AnimationPropertyType', 'AnimationRange', 'AnimationStatus', + 'AnimatorInterface', 'AppRotation', 'AppStorage', 'AppearSymbolEffect', 'Area', + 'ArrowPointPosition', 'ArrowPosition', 'ArrowStyle', 'AutoCapitalizationMode', + 'AutoPlayOptions', 'AvailableLayoutArea', 'AvoidanceMode', 'Axis', 'AxisAction', + 'AxisEvent', 'AxisModel', 'BackgroundBlurStyleOptions', 'BackgroundBrightnessOptions', + 'BackgroundColorStyle', 'BackgroundEffectOptions', 'BackgroundImageOptions', 'BadgeParam', + 'BadgeParamWithNumber', 'BadgeParamWithString', 'BadgePosition', 'BadgeStyle', + 'BarGridColumnOptions', 'BarMode', 'BarPosition', 'BarState', 'BarStyle', + 'BarrierDirection', 'BarrierStyle', 'BaseCustomComponent', 'BaseEvent', 'BaseGestureEvent', + 'BaseHandlerOptions', 'BaseShape', 'BaseSpan', 'BaselineOffsetStyle', 'Bias', 'BindOptions', + 'BlendApplyType', 'BlendMode', 'Blender', 'BlurOptions', 'BlurStyle', + 'BlurStyleActivePolicy', 'BlurStyleOptions', 'BoardStyle', 'BorderImageOption', + 'BorderOptions', 'BorderRadiuses', 'BorderStyle', 'BottomTabBarStyle', 'BounceSymbolEffect', + 'BreakPoints', 'BreakpointsReference', 'Builder', 'BuilderAttachment', + 'BuilderAttachmentInterface', 'BuilderParam', 'BusinessError', 'ButtonConfiguration', + 'ButtonIconOptions', 'ButtonOptions', 'ButtonRole', 'ButtonStyle', 'ButtonStyleMode', + 'ButtonTriggerClickCallback', 'ButtonType', 'CacheMode', 'CalendarAlign', + 'CalendarController', 'CalendarDay', 'CalendarDialogOptions', 'CalendarOptions', + 'CalendarPickerDialog', 'CalendarRequestedData', 'CalendarSelectedDate', 'Callback', + 'CallbackBuffer', 'CallbackKind', 'CallbackResource', 'CallbackResourceHolder', + 'CancelButtonOptions', 'CancelButtonStyle', 'CancelButtonSymbolOptions', 'CanvasDirection', + 'CanvasFillRule', 'CanvasGradient', 'CanvasLineCap', 'CanvasLineJoin', 'CanvasOptions', + 'CanvasPath', 'CanvasPattern', 'CanvasRenderer', 'CanvasRenderingContext2D', + 'CanvasTextAlign', 'CanvasTextBaseline', 'CapsuleStyleOptions', 'CaretOffset', 'CaretStyle', + 'ChainAnimationOptions', 'ChainEdgeEffect', 'ChainStyle', 'ChainWeightOptions', + 'CheckBoxConfiguration', 'CheckBoxShape', 'CheckboxGroupOptions', 'CheckboxGroupResult', + 'CheckboxOptions', 'ChildHitFilterOption', 'ChildrenMainSize', 'CircleOptions', + 'CircleShape', 'CircleStyleOptions', 'ClickEffect', 'ClickEffectLevel', 'ClickEvent', + 'ClientAuthenticationHandler', 'CloseSwipeActionOptions', 'Color', 'ColorContent', + 'ColorFilter', 'ColorMetrics', 'ColorMode', 'ColorSpace', 'ColorStop', 'ColoringStrategy', + 'ColumnOptions', 'ColumnOptionsV2', 'ColumnSplitDividerStyle', 'CommonConfiguration', + 'CommonProgressStyleOptions', 'CommonShape', 'CommonShapeMethod', 'CommonTransition', + 'ComponentContent', 'ComponentOptions', 'ComponentRoot', 'Configuration', + 'ConsoleMessage', 'ConstraintSizeOptions', 'Content', 'ContentClipMode', 'ContentCoverOptions', + 'ContentDidScrollCallback', 'ContentType', 'Context', 'ContextMenu', + 'ContextMenuAnimationOptions', 'ContextMenuEditStateFlags', 'ContextMenuInputFieldType', + 'ContextMenuMediaType', 'ContextMenuOptions', 'ContextMenuSourceType', 'ControlSize', + 'ControllerHandler', 'CopyEvent', 'CopyOptions', 'CrownAction', 'CrownEvent', + 'CrownSensitivity', 'CurrentDayStyle', 'Curve', 'CustomBuilder', 'CustomComponent', + 'CustomComponentV2', 'CustomDialogController', 'CustomDialogControllerOptions', + 'CustomNodeBuilder', 'CustomPopupOptions', 'CustomSpan', 'CustomSpanDrawInfo', + 'CustomSpanMeasureInfo', 'CustomSpanMetrics', 'CustomTheme', 'CutEvent', + 'DataAddOperation', 'DataChangeListener', 'DataChangeOperation', 'DataDeleteOperation', + 'DataExchangeOperation', 'DataMoveOperation', 'DataOperation', 'DataOperationType', + 'DataPanelConfiguration', 'DataPanelOptions', 'DataPanelShadowOptions', 'DataPanelType', + 'DataReloadOperation', 'DataResubmissionHandler', 'DatePickerDialog', + 'DatePickerDialogOptions', 'DatePickerMode', 'DatePickerOptions', 'DatePickerResult', + 'DateRange', 'DateTimeOptions', 'DecorationStyle', 'DecorationStyleInterface', + 'DecorationStyleResult', 'Degree', 'DeleteValue', 'Deserializer', 'DialogAlignment', + 'DialogButtonDirection', 'DialogButtonStyle', 'DialogDisplayMode', 'DigitIndicator', + 'Dimension', 'Direction', 'DirectionalEdgesT', 'DisableSymbolEffect', 'DisappearSymbolEffect', + 'DismissContentCoverAction', 'DismissContinueReason', 'DismissDialogAction', + 'DismissFollowUpAction', 'DismissMenuAction', 'DismissPopupAction', 'DismissReason', + 'DismissSheetAction', 'DistributionType', 'DisturbanceFieldOptions', 'DisturbanceFieldShape', + 'DividerMode', 'DividerOptions', 'DividerStyle', 'DividerStyleOptions', 'DotIndicator', + 'DoubleAnimationParam', 'DpiFollowStrategy', 'DragBehavior', 'DragEvent', + 'DragInteractionOptions', 'DragItemInfo', 'DragPointCoordinate', 'DragPreviewLiftingScale', + 'DragPreviewMode', 'DragPreviewOptions', 'DragResult', 'DraggingSizeChangeEffect', + 'DrawContext', 'DrawModifier', 'DrawableDescriptor', 'DrawingCanvas', 'DrawingColorFilter', + 'DrawingLattice', 'DrawingRenderingContext', 'DropOptions', 'DynamicNode', + 'DynamicRangeMode', 'EclipseStyleOptions', 'Edge', 'EdgeColors', 'EdgeEffect', + 'EdgeEffectOptions', 'EdgeOutlineStyles', 'EdgeOutlineWidths', 'EdgeStyles', 'EdgeWidth', + 'EdgeWidths', 'Edges', 'EditMenuOptions', 'EditMode', 'EditableTextChangeValue', + 'EditableTextOnChangeCallback', 'EffectDirection', 'EffectEdge', 'EffectFillStyle', + 'EffectScope', 'EffectType', 'EllipseOptions', 'EllipseShape', 'EllipsisMode', + 'EmbeddedDpiFollowStrategy', 'EmbeddedOptions', 'EmbeddedType', + 'EmbeddedWindowModeFollowStrategy', 'EmitterOptions', 'EmitterParticleOptions', + 'EmitterProperty', 'EnterKeyType', 'Entry', 'EntryOptions', 'EnvPropsOptions', + 'Environment', 'ErrorCallback', 'Event', 'EventEmulator', 'EventLocationInfo', + 'EventQueryType', 'EventResult', 'EventTarget', 'EventTargetInfo', 'ExchangeIndex', + 'ExchangeKey', 'ExpandedMenuItemOptions', 'ExpectedFrameRateRange', 'Extend', 'FP', + 'FadingEdgeOptions', 'FileSelectorMode', 'FileSelectorParam', 'FileSelectorResult', + 'FillMode', 'Filter', 'FingerInfo', 'FinishCallbackType', 'FirstMeaningfulPaint', + 'FlexAlign', 'FlexDirection', 'FlexOptions', 'FlexSpaceOptions', 'FlexWrap', + 'FocusAxisEvent', 'FocusBoxStyle', 'FocusController', 'FocusDrawLevel', 'FocusMovement', + 'FocusPriority', 'FocusWrapMode', 'FoldStatus', 'FolderStackOptions', 'Font', + 'FontInfo', 'FontOptions', 'FontSettingOptions', 'FontStyle', 'FontWeight', + 'ForegroundBlurStyleOptions', 'ForegroundEffectOptions', 'FormCallbackInfo', + 'FormDimension', 'FormInfo', 'FormLinkOptions', 'FormRenderingMode', 'FormShape', + 'FractionStop', 'FrameNode', 'FrictionMotion', 'FullScreenEnterEvent', + 'FullScreenExitHandler', 'FullscreenInfo', 'FunctionKey', 'GaugeConfiguration', + 'GaugeIndicatorOptions', 'GaugeOptions', 'GaugeShadowOptions', 'GeometryInfo', + 'GeometryTransitionOptions', 'Gesture', 'GestureControl', 'GestureEvent', 'GestureGroup', + 'GestureGroupGestureHandlerOptions', 'GestureGroupHandler', 'GestureHandler', + 'GestureInfo', 'GestureJudgeResult', 'GestureMask', 'GestureMode', 'GestureModifier', + 'GesturePriority', 'GestureRecognizer', 'GestureRecognizerJudgeBeginCallback', + 'GestureRecognizerState', 'GestureStyle', 'GestureType', 'GetItemMainSizeByIndex', + 'GradientDirection', 'GridColColumnOption', 'GridColOptions', 'GridContainerOptions', + 'GridDirection', 'GridItemAlignment', 'GridItemOptions', 'GridItemStyle', + 'GridLayoutOptions', 'GridRowColumnOption', 'GridRowDirection', 'GridRowOptions', + 'GridRowSizeOption', 'GuideLinePosition', 'GuideLineStyle', 'GutterOption', + 'HapticFeedbackMode', 'Header', 'HeightBreakpoint', 'HierarchicalSymbolEffect', + 'HistoricalPoint', 'HitTestMode', 'HitTestType', 'HorizontalAlign', 'HoverCallback', + 'HoverEffect', 'HoverEvent', 'HoverEventParam', 'HoverModeAreaType', 'HttpAuthHandler', + 'ICurve', 'IDataSource', 'IMonitor', 'IMonitorValue', 'IPropertySubscriber', + 'ISinglePropertyChangeSubscriber', 'IconOptions', 'IlluminatedType', 'ImageAIOptions', + 'ImageAnalyzerConfig', 'ImageAnalyzerController', 'ImageAnalyzerType', 'ImageAttachment', + 'ImageAttachmentInterface', 'ImageAttachmentLayoutStyle', 'ImageBitmap', + 'ImageCompleteCallback', 'ImageContent', 'ImageData', 'ImageError', 'ImageErrorCallback', + 'ImageFit', 'ImageFrameInfo', 'ImageInterpolation', 'ImageLoadResult', 'ImageModifier', + 'ImageParticleParameters', 'ImageRenderMode', 'ImageRepeat', 'ImageRotateOrientation', + 'ImageSize', 'ImageSmoothingQuality', 'ImageSourceSize', 'ImageSpanAlignment', + 'IndexerAlign', 'Indicator', 'IndicatorComponentController', 'IndicatorStyle', + 'InputCounterOptions', 'InputType', 'InsertValue', 'IntelligentTrackingPreventionDetails', + 'IntentionCode', 'InteractionHand', 'InterceptionModeCallback', 'InterceptionShowCallback', + 'Interop', 'InvertOptions', 'IsolatedOptions', 'ItemAlign', 'ItemDragEventHandler', + 'ItemDragInfo', 'ItemState', 'JavaScriptProxy', 'JsGeolocation', 'JsResult', 'KVMContext', + 'KeyEvent', 'KeyProcessingMode', 'KeySource', 'KeyType', 'KeyboardAppearance', + 'KeyboardAvoidMode', 'KeyboardOptions', 'KeyframeAnimateParam', 'KeyframeState', 'LPX', + 'LabelStyle', 'LargestContentfulPaint', 'LaunchMode', 'LayoutBorderInfo', 'LayoutChild', + 'LayoutDirection', 'LayoutInfo', 'LayoutManager', 'LayoutMode', 'LayoutPolicy', + 'LayoutSafeAreaEdge', 'LayoutSafeAreaType', 'LayoutStyle', 'Layoutable', 'LazyForEachOps', + 'LeadingMarginPlaceholder', 'Length', 'LengthConstrain', 'LengthMetrics', + 'LengthMetricsUnit', 'LengthUnit', 'LetterSpacingStyle', 'LightSource', + 'LineBreakStrategy', 'LineCapStyle', 'LineHeightStyle', 'LineJoinStyle', 'LineMetrics', + 'LineOptions', 'LineSpacingOptions', 'LinearGradient', 'LinearGradientBlurOptions', + 'LinearGradientOptions', 'LinearIndicatorController', 'LinearIndicatorStartOptions', + 'LinearIndicatorStyle', 'LinearStyleOptions', 'ListDividerOptions', 'ListItemAlign', + 'ListItemGroupArea', 'ListItemGroupOptions', 'ListItemGroupStyle', 'ListItemOptions', + 'ListItemStyle', 'ListOptions', 'ListScroller', 'LoadCommittedDetails', 'Loader', + 'LoadingProgressConfiguration', 'LoadingProgressStyle', 'LocalBuilder', 'LocalStorage', + 'LocalizedAlignRuleOptions', 'LocalizedAlignment', 'LocalizedBarrierDirection', + 'LocalizedBarrierStyle', 'LocalizedBorderRadiuses', 'LocalizedDragPointCoordinate', + 'LocalizedEdgeColors', 'LocalizedEdgeWidths', 'LocalizedEdges', + 'LocalizedHorizontalAlignParam', 'LocalizedMargin', 'LocalizedPadding', + 'LocalizedPosition', 'LocalizedVerticalAlignParam', 'LocationDescription', + 'LocationIconStyle', 'LongPressGesture', 'LongPressGestureEvent', 'LongPressGestureHandler', + 'LongPressGestureHandlerOptions', 'LongPressGestureParams', 'LongPressRecognizer', + 'LunarSwitchStyle', 'Margin', 'MarkStyle', 'MarqueeOptions', 'MarqueeStartPolicy', + 'MarqueeState', 'MarqueeUpdateStrategy', 'Materialized', 'Matrix2D', 'MaxLinesMode', + 'MaxLinesOptions', 'Measurable', 'MeasureOptions', 'MeasureResult', 'MenuAlignType', + 'MenuElement', 'MenuItemConfiguration', 'MenuItemGroupOptions', 'MenuItemOptions', + 'MenuItemOptionsV2', 'MenuMaskType', 'MenuOnAppearCallback', 'MenuOptions', + 'MenuOutlineOptions', 'MenuPolicy', 'MenuPreviewMode', 'MenuType', 'MessageLevel', + 'MixedMode', 'ModalMode', 'ModalTransition', 'ModelType', 'ModifierKey', 'Monitor', + 'MonthData', 'MoreButtonOptions', 'MotionBlurAnchor', 'MotionBlurOptions', + 'MotionPathOptions', 'MouseAction', 'MouseButton', 'MouseEvent', 'MoveIndex', + 'MultiShadowOptions', 'MutableStyledString', 'NativeEmbedDataInfo', 'NativeEmbedInfo', + 'NativeEmbedStatus', 'NativeEmbedTouchInfo', 'NativeEmbedVisibilityInfo', + 'NativeMediaPlayerConfig', 'NativeXComponentParameters', 'NavBar', 'NavBarPosition', + 'NavContentInfo', 'NavDestinationActiveReason', 'NavDestinationCommonTitle', + 'NavDestinationContext', 'NavDestinationCustomTitle', 'NavDestinationInfo', + 'NavDestinationMode', 'NavDestinationTransition', 'NavExtender', 'NavPathInfo', + 'NavPathStack', 'NavRouteMode', 'NavigationAnimatedTransition', 'NavigationCommonTitle', + 'NavigationCustomTitle', 'NavigationDividerStyle', 'NavigationInfo', + 'NavigationInterception', 'NavigationMenuItem', 'NavigationMenuOptions', + 'NavigationMode', 'NavigationOperation', 'NavigationOptions', + 'NavigationSystemTransitionType', 'NavigationTitleMode', 'NavigationTitleOptions', + 'NavigationToolbarOptions', 'NavigationTransitionProxy', 'NavigationType', + 'NestedScrollInfo', 'NestedScrollMode', 'NestedScrollOptions', 'NestedScrollOptionsExt', + 'Node', 'NodeController', 'NonCurrentDayStyle', 'Nullable', 'ObscuredReasons', + 'OffscreenCanvas', 'OffscreenCanvasRenderingContext2D', 'Offset', 'OffsetOptions', + 'OffsetResult', 'OnAdsBlockedCallback', 'OnAlertEvent', 'OnAlphabetIndexerPopupSelectCallback', + 'OnAlphabetIndexerRequestPopupDataCallback', 'OnAlphabetIndexerSelectCallback', + 'OnAudioStateChangedEvent', 'OnBeforeUnloadEvent', 'OnCheckboxChangeCallback', + 'OnCheckboxGroupChangeCallback', 'OnClientAuthenticationEvent', 'OnConfirmEvent', + 'OnConsoleEvent', 'OnContentScrollCallback', 'OnContextMenuHideCallback', + 'OnContextMenuShowEvent', 'OnDataResubmittedEvent', 'OnDidChangeCallback', + 'OnDownloadStartEvent', 'OnErrorReceiveEvent', 'OnFaviconReceivedEvent', + 'OnFirstContentfulPaintEvent', 'OnFirstMeaningfulPaintCallback', 'OnFoldStatusChangeCallback', + 'OnFoldStatusChangeInfo', 'OnFullScreenEnterCallback', 'OnGeolocationShowEvent', + 'OnHoverStatusChangeCallback', 'OnHttpAuthRequestEvent', 'OnHttpErrorReceiveEvent', + 'OnIntelligentTrackingPreventionCallback', 'OnInterceptRequestEvent', + 'OnLargestContentfulPaintCallback', 'OnLinearIndicatorChangeCallback', 'OnLoadInterceptEvent', + 'OnMoveHandler', 'OnNativeEmbedVisibilityChangeCallback', 'OnNativeLoadCallback', + 'OnNavigationEntryCommittedCallback', 'OnOverScrollEvent', 'OnOverrideUrlLoadingCallback', + 'OnPageBeginEvent', 'OnPageEndEvent', 'OnPageVisibleEvent', 'OnPasteCallback', + 'OnPermissionRequestEvent', 'OnProgressChangeEvent', 'OnPromptEvent', + 'OnRefreshAccessedHistoryEvent', 'OnRenderExitedEvent', + 'OnRenderProcessNotRespondingCallback', 'OnRenderProcessRespondingCallback', + 'OnResourceLoadEvent', 'OnSafeBrowsingCheckResultCallback', 'OnScaleChangeEvent', + 'OnScreenCaptureRequestEvent', 'OnScrollCallback', 'OnScrollEdgeCallback', 'OnScrollEvent', + 'OnScrollFrameBeginCallback', 'OnScrollFrameBeginHandlerResult', + 'OnScrollVisibleContentChangeCallback', 'OnSearchResultReceiveEvent', + 'OnShowFileSelectorEvent', 'OnSslErrorEventCallback', 'OnSslErrorEventReceiveEvent', + 'OnSubmitCallback', 'OnSwiperAnimationEndCallback', 'OnSwiperAnimationStartCallback', + 'OnSwiperGestureSwipeCallback', 'OnTabsAnimationEndCallback', 'OnTabsAnimationStartCallback', + 'OnTabsContentWillChangeCallback', 'OnTabsGestureSwipeCallback', + 'OnTextSelectionChangeCallback', 'OnTitleReceiveEvent', 'OnTouchIconUrlReceivedEvent', + 'OnViewportFitChangedCallback', 'OnWillScrollCallback', 'OnWindowNewEvent', 'Once', + 'OptionWidthMode', 'Optional', 'OutlineOptions', 'OutlineRadiuses', 'OutlineStyle', + 'OverScrollMode', 'OverlayOffset', 'OverlayOptions', 'PX', 'Padding', 'PageFlipMode', + 'PageTransitionCallback', 'PageTransitionEnter', 'PageTransitionExit', + 'PageTransitionOptions', 'PanDirection', 'PanGesture', 'PanGestureEvent', + 'PanGestureHandler', 'PanGestureHandlerOptions', 'PanGestureOptions', 'PanGestureParams', + 'PanRecognizer', 'PanelHeight', 'PanelMode', 'PanelType', 'ParagraphStyle', + 'ParagraphStyleInterface', 'ParticleAnnulusRegion', 'ParticleColorOptions', + 'ParticleColorPropertyOptions', 'ParticleColorPropertyUpdaterConfigs', + 'ParticleColorUpdaterOptions', 'ParticleConfigs', 'ParticleEmitterShape', + 'ParticleOptions', 'ParticlePropertyAnimation', 'ParticlePropertyOptions', + 'ParticlePropertyUpdaterConfigs', 'ParticleTuple', 'ParticleType', 'ParticleUpdater', + 'ParticleUpdaterOptions', 'Particles', 'PasswordIcon', 'PasteButtonOnClickResult', + 'PasteButtonOptions', 'PasteDescription', 'PasteEvent', 'PasteEventCallback', + 'PasteIconStyle', 'Path2D', 'PathOptions', 'PathShape', 'PathShapeOptions', + 'PatternLockChallengeResult', 'PatternLockController', 'Percentage', + 'PerfMonitorActionType', 'PerfMonitorSourceType', 'PermissionRequest', + 'PersistPropsOptions', 'PersistentStorage', 'PickerBackgroundStyle', + 'PickerDialogButtonStyle', 'PickerTextStyle', 'PinchGesture', 'PinchGestureEvent', + 'PinchGestureHandler', 'PinchGestureHandlerOptions', 'PinchGestureParams', + 'PinchRecognizer', 'PixelMap', 'PixelMapMock', 'PixelRoundCalcPolicy', 'PixelRoundMode', + 'PixelRoundPolicy', 'PixelStretchEffectOptions', 'PlaceholderStyle', 'Placement', + 'PlayMode', 'PlaybackInfo', 'PlaybackSpeed', 'PluginComponentOptions', + 'PluginComponentTemplate', 'PluginErrorCallback', 'PluginErrorData', 'Point', + 'PointLightStyle', 'PointParticleParameters', 'PointerStyle', 'PolygonOptions', + 'PolylineOptions', 'PopInfo', 'PopupBorderLinearGradient', 'PopupCommonOptions', + 'PopupMaskType', 'PopupMessageOptions', 'PopupOptions', 'PopupStateChangeParam', + 'Position', 'PositionT', 'PositionWithAffinity', 'PosterOptions', 'PreDragStatus', + 'PreparedInfo', 'Preview', 'PreviewConfiguration', 'PreviewMenuOptions', 'PreviewParams', + 'PreviewText', 'Profiler', 'ProgressConfiguration', 'ProgressMask', 'ProgressOptions', + 'ProgressStatus', 'ProgressStyle', 'ProgressStyleMap', 'ProgressStyleOptions', + 'ProgressType', 'Prop', 'ProtectedResourceType', 'Provide', 'ProvideOptions', + 'Provider', 'PulseSymbolEffect', 'QuickReplaceSymbolEffect', 'RRect', + 'RadialGradientOptions', 'RadioConfiguration', 'RadioIndicatorType', 'RadioOptions', + 'RadioStyle', 'RatingConfiguration', 'RatingOptions', 'RawFileDescriptor', + 'ReceiveCallback', 'RectHeightStyle', 'RectOptions', 'RectResult', 'RectShape', + 'RectShapeOptions', 'RectWidthStyle', 'Rectangle', 'RefreshOptions', 'RefreshStatus', + 'RelateType', 'RenderExitReason', 'RenderFit', 'RenderMode', + 'RenderProcessNotRespondingData', 'RenderProcessNotRespondingReason', + 'RenderingContextSettings', 'RepeatItem', 'RepeatMode', 'ReplaceSymbolEffect', + 'ResizableOptions', 'ResolutionQuality', 'Resource', 'ResourceColor', + 'ResourceImageAttachmentOptions', 'ResourceStr', 'ResponseType', 'RestrictedWorker', + 'ReuseOptions', 'RichEditorBaseController', 'RichEditorBuilderSpanOptions', + 'RichEditorChangeValue', 'RichEditorController', 'RichEditorDeleteDirection', + 'RichEditorDeleteValue', 'RichEditorGesture', 'RichEditorImageSpan', + 'RichEditorImageSpanOptions', 'RichEditorImageSpanResult', 'RichEditorImageSpanStyle', + 'RichEditorImageSpanStyleResult', 'RichEditorInsertValue', 'RichEditorLayoutStyle', + 'RichEditorOptions', 'RichEditorParagraphResult', 'RichEditorParagraphStyle', + 'RichEditorParagraphStyleOptions', 'RichEditorRange', 'RichEditorResponseType', + 'RichEditorSelection', 'RichEditorSpan', 'RichEditorSpanPosition', + 'RichEditorSpanStyleOptions', 'RichEditorSpanType', 'RichEditorStyledStringController', + 'RichEditorStyledStringOptions', 'RichEditorSymbolSpanOptions', + 'RichEditorSymbolSpanStyle', 'RichEditorSymbolSpanStyleResult', 'RichEditorTextSpan', + 'RichEditorTextSpanOptions', 'RichEditorTextSpanResult', 'RichEditorTextStyle', + 'RichEditorTextStyleResult', 'RichEditorUpdateImageSpanStyleOptions', + 'RichEditorUpdateSymbolSpanStyleOptions', 'RichEditorUpdateTextSpanStyleOptions', + 'RichEditorUrlStyle', 'RingStyleOptions', 'Root', 'RootSceneSession', 'RotateOptions', + 'RotationGesture', 'RotationGestureEvent', 'RotationGestureHandler', + 'RotationGestureHandlerOptions', 'RotationGestureParams', 'RotationRecognizer', + 'RoundRectShapeOptions', 'RoundedRectOptions', 'RouteInfo', 'RouteMapConfig', + 'RouteType', 'RouterPageInfo', 'RowOptions', 'RowOptionsV2', 'RuntimeType', + 'SafeAreaEdge', 'SafeAreaType', 'SaveButtonOnClickResult', 'SaveButtonOptions', + 'SaveDescription', 'SaveIconStyle', 'ScaleOptions', 'ScaleRingStyleOptions', + 'ScaleSymbolEffect', 'ScanEffectOptions', 'Scene', 'SceneOptions', 'ScreenCaptureConfig', + 'ScreenCaptureHandler', 'ScriptItem', 'ScrollAlign', 'ScrollAnimationOptions', + 'ScrollBarDirection', 'ScrollBarMargin', 'ScrollBarOptions', 'ScrollDirection', + 'ScrollEdgeOptions', 'ScrollMotion', 'ScrollOnScrollCallback', 'ScrollOnWillScrollCallback', + 'ScrollOptions', 'ScrollPageOptions', 'ScrollResult', 'ScrollSizeMode', + 'ScrollSnapAlign', 'ScrollSnapOptions', 'ScrollSource', 'ScrollState', + 'ScrollToIndexOptions', 'ScrollableBarModeOptions', 'ScrollableCommonMethod', + 'ScrollableTargetInfo', 'Scroller', 'SearchButtonOptions', 'SearchController', + 'SearchOptions', 'SearchSubmitCallback', 'SearchType', 'SectionOptions', + 'SecurityComponentLayoutDirection', 'SecurityComponentMethod', 'SeekMode', + 'SelectOption', 'SelectStatus', 'SelectedMode', 'SelectionMenuOptions', + 'SelectionMenuOptionsExt', 'SelectionOptions', 'Serializer', 'ShadowOptions', + 'ShadowStyle', 'ShadowType', 'ShapeSize', 'SharedTransitionEffectType', 'SheetDismiss', + 'SheetInfo', 'SheetKeyboardAvoidMode', 'SheetMode', 'SheetOptions', 'SheetSize', + 'SheetTitleOptions', 'SheetType', 'ShouldBuiltInRecognizerParallelWithCallback', + 'SideBarContainerType', 'SideBarPosition', 'Size', 'SizeChangeCallback', 'SizeOptions', + 'SizeResult', 'SizeT', 'SizeType', 'SlideEffect', 'SlideRange', 'SliderBlockStyle', + 'SliderBlockType', 'SliderChangeMode', 'SliderConfiguration', 'SliderCustomContentOptions', + 'SliderInteraction', 'SliderOptions', 'SliderPrefixOptions', 'SliderShowStepOptions', + 'SliderStepItemAccessibility', 'SliderStyle', 'SliderSuffixOptions', + 'SliderTriggerChangeCallback', 'SnapshotOptions', 'SourceTool', 'SourceType', + 'SpanStyle', 'SpringBackAction', 'SpringMotion', 'SpringProp', 'SslError', + 'SslErrorEvent', 'SslErrorHandler', 'StackOptions', 'StarStyleOptions', 'State', + 'StateStyles', 'Sticky', 'StickyStyle', 'Storage', 'StyleOptions', 'StyledString', + 'StyledStringChangeValue', 'StyledStringChangedListener', 'StyledStringController', + 'StyledStringKey', 'StyledStringValue', 'Styles', 'SubMenuExpandingMode', + 'SubTabBarStyle', 'SubmitCallback', 'SubmitEvent', 'SubscribaleAbstract', + 'SubscribedAbstractProperty', 'Summary', 'SuperscriptStyle', 'SurfaceRect', + 'SurfaceRotationOptions', 'SweepGradientOptions', 'SwipeActionItem', 'SwipeActionOptions', + 'SwipeActionState', 'SwipeDirection', 'SwipeEdgeEffect', 'SwipeGesture', + 'SwipeGestureEvent', 'SwipeGestureHandler', 'SwipeGestureHandlerOptions', + 'SwipeGestureParams', 'SwipeRecognizer', 'Swiper', 'SwiperAnimationEvent', + 'SwiperAnimationMode', 'SwiperAutoFill', 'SwiperContentAnimatedTransition', + 'SwiperContentTransitionProxy', 'SwiperContentWillScrollResult', 'SwiperController', + 'SwiperDisplayMode', 'SwiperNestedScrollMode', 'SwitchStyle', 'SymbolEffect', + 'SymbolEffectStrategy', 'SymbolGlyphModifier', 'SymbolRenderingStrategy', + 'SyncedPropertyOneWay', 'SyncedPropertyTwoWay', 'SystemAdaptiveOptions', + 'SystemBarStyle', 'SystemOps', 'TabBarIconStyle', 'TabBarOptions', 'TabBarSymbol', + 'TabContentAnimatedTransition', 'TabContentTransitionProxy', 'TabsAnimationEvent', + 'TabsCacheMode', 'TabsController', 'TabsCustomContentTransitionCallback', + 'TabsOptions', 'Tag', 'TapGesture', 'TapGestureEvent', 'TapGestureHandler', + 'TapGestureHandlerOptions', 'TapGestureParameters', 'TapGestureParams', + 'TapRecognizer', 'TemplateOptions', 'TerminationInfo', 'Test', 'TextAlign', + 'TextAreaController', 'TextAreaOptions', 'TextAreaSubmitCallback', 'TextAreaType', + 'TextBackgroundStyle', 'TextBaseController', 'TextBox', 'TextCascadePickerRangeContent', + 'TextCase', 'TextChangeOptions', 'TextChangeReason', 'TextClockConfiguration', + 'TextClockController', 'TextClockOptions', 'TextContentControllerBase', + 'TextContentControllerOptions', 'TextContentStyle', 'TextController', + 'TextDataDetectorConfig', 'TextDataDetectorType', 'TextDecorationOptions', + 'TextDecorationStyle', 'TextDecorationType', 'TextDeleteDirection', + 'TextEditControllerEx', 'TextHeightAdaptivePolicy', 'TextLayoutOptions', + 'TextInputController', 'TextInputOptions', 'TextInputStyle', 'TextMarqueeOptions', + 'TextMenuItem', 'TextMenuItemId', 'TextMenuOptions', 'TextMenuShowMode', 'TextMetrics', + 'TextModifier', 'TextOptions', 'TextOverflow', 'TextOverflowOptions', + 'TextPickerDialog', 'TextPickerDialogOptions', 'TextPickerOptions', + 'TextPickerRangeContent', 'TextPickerResult', 'TextPickerTextStyle', 'TextRange', + 'TextResponseType', 'TextSelectableMode', 'TextShadowStyle', 'TextSpanType', + 'TextStyle', 'TextTimerConfiguration', 'TextTimerController', 'TextTimerOptions', + 'Theme', 'ThemeColorMode', 'ThreatType', 'TimePickerDialog', 'TimePickerDialogOptions', + 'TimePickerFormat', 'TimePickerOptions', 'TimePickerResult', 'TipsOptions', + 'TitleHeight', 'TodayStyle', 'ToggleConfiguration', 'ToggleOptions', 'ToggleType', + 'ToolBarItemInterface', 'ToolBarItemOptions', 'ToolBarItemPlacement', 'ToolbarItem', + 'ToolbarItemStatus', 'TouchEvent', 'TouchObject', 'TouchPoint', 'TouchResult', + 'TouchTestInfo', 'TouchTestStrategy', 'TouchType', 'Trace', 'Track', 'TransitionEdge', + 'TransitionEffect', 'TransitionEffects', 'TransitionFinishCallback', + 'TransitionHierarchyStrategy', 'TransitionOptions', 'TransitionType', + 'TranslateOptions', 'UICommonEvent', 'UIContext', 'UIExtensionOptions', + 'UIExtensionProxy', 'UIGestureEvent', 'UnderlineColor', 'UnifiedData', + 'UniformDataType', 'UrlStyle', 'UserDataSpan', 'VMContext', 'VP', 'VelocityOptions', + 'VerticalAlign', 'VideoController', 'VideoOptions', 'View', 'ViewportFit', + 'ViewportRect', 'VirtualScrollOptions', 'Visibility', 'VisibleAreaChangeCallback', + 'VisibleAreaEventOptions', 'VisibleListContentInfo', 'VisualEffect', 'VoiceButtonOptions', + 'VoidCallback', 'Want', 'Watch', 'WaterFlowLayoutMode', 'WaterFlowOptions', + 'WaterFlowSections', 'WebCaptureMode', 'WebContextMenuParam', 'WebContextMenuResult', + 'WebController', 'WebCookie', 'WebDarkMode', 'WebElementType', 'WebHeader', + 'WebKeyboardAvoidMode', 'WebKeyboardCallback', 'WebKeyboardCallbackInfo', + 'WebKeyboardController', 'WebKeyboardOptions', 'WebLayoutMode', 'WebMediaOptions', + 'WebNavigationType', 'WebOptions', 'WebResourceError', 'WebResourceRequest', + 'WebResourceResponse', 'WebResponseType', 'WebviewController', 'Week', 'WeekStyle', + 'WidthBreakpoint', 'WindowAnimationTarget', 'WindowModeFollowStrategy', + 'WindowStatusType', 'WithThemeOptions', 'WordBreak', 'WorkStateStyle', 'WrappedBuilder', + 'XComponentController', 'XComponentOptions', 'XComponentType', 'animateTo', + 'animateToImmediately', 'cursorControl', 'focusControl', 'fp2px', 'getContext', + 'getInspectorNodeById', 'getInspectorNodes', 'lpx2px', 'postCardAction', 'px2fp', + 'px2lpx', 'px2vp', 'setAppBgColor', 'sharedTransitionOptions', 'vp2px' +]); + +const decoratorsWhiteList = [ + 'State', 'Prop', 'Link', 'Observed', 'Track', 'ObjectLink', 'StorageProp', 'StorageLink', + 'LocalStorageProp', 'LocalStorageLink', 'Provide', 'Consume', 'Watch', + 'Local', 'Param', 'Once', 'Event', 'Provider', 'Consumer', 'Monitor', 'Computed', '@ObservedV2', 'Trace', + 'Builder', 'BuildParam', 'Styles', 'Extend', 'AnimatableExtend', 'Type', 'Require', + 'Reusable', 'ReusableV2', 'Entry', 'Component', 'ComponentV2', 'CustomDialog' +]; + +const whiteFileList = [ + '@ohos.app.ability.continueManager', + '@ohos.app.ability.InsightIntentDecorator', + '@ohos.app.ability.UIExtensionContentSession', + '@ohos.distributedsched.proxyChannelManager', + '@ohos.graphics.displaySync', + '@ohos.graphics.drawing', + '@ohos.graphics.text', + '@ohos.i18n', + '@ohos.inputMethodEngine', + '@ohos.userIAM.userAuth', + '@ohos.web.webview', + 'LiveFormExtensionContext', + 'Scene', + 'SceneResources', + 'UIAbilityContext', +]; + +const apiDir = [ + 'ability', 'advertising', 'app', 'application', 'arkui', 'bundle', 'bundleManager', 'commonEvent', + 'continuation', 'data', 'global', 'graphics3d', 'multimedia', 'notification', 'security', 'tag', + 'wantAgent' +]; + +const apiInternalDir = [ + '@internal/full' +]; + +export { + apiDir, + apiInternalDir, + whiteList, + decoratorsWhiteList, + whiteFileList, +}; diff --git a/arkui_noninterop_plugin/src/collect_symbol/config.ts b/arkui_noninterop_plugin/src/collect_symbol/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..630de068b0015c2f812551d947f8c41dd34a6f90 --- /dev/null +++ b/arkui_noninterop_plugin/src/collect_symbol/config.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 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 const input: string = ''; +export const output: string = './export.ts'; diff --git a/arkui_noninterop_plugin/src/collect_symbol/index.ts b/arkui_noninterop_plugin/src/collect_symbol/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3693e104e62ebaa1b404bdb6d37563ca2e69902e --- /dev/null +++ b/arkui_noninterop_plugin/src/collect_symbol/index.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +import * as ts from 'typescript'; + +import { + input, + output, +} from './config'; + +function readTsFile(filePath: string): string { + const absolutePath = path.resolve(filePath); + return fs.readFileSync(absolutePath, 'utf8'); +} + +function traverseAst(node: ts.Node, result: Set): void { + if (ts.isClassDeclaration(node) && node.name) { + result.add(node.name.text); + } + + if (ts.isEnumDeclaration(node) && node.name) { + result.add(node.name.text); + } + + ts.forEachChild(node, child => traverseAst(child, result)); +} + +function generateExportFile(identifiers: Array, outputPath: string): void { + if (identifiers.length === 0) { + return; + } + + const exportContent = `export { ${identifiers.join(', ')} };\n`; + fs.writeFileSync(outputPath, exportContent, 'utf8'); +} + +function main(inputFilePath: string, outputFilePath: string): void { + const fileContent = readTsFile(inputFilePath); + + const sourceFile = ts.createSourceFile( + inputFilePath, + fileContent, + ts.ScriptTarget.ESNext, + true + ); + + const identifiers: Set = new Set(); + traverseAst(sourceFile, identifiers); + + generateExportFile(Array.from(identifiers).sort(), outputFilePath); +} + +main(input, output); diff --git a/arkui_noninterop_plugin/src/delete_noninterop/add_export.ts b/arkui_noninterop_plugin/src/delete_noninterop/add_export.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ce4792fcd87213cee4f946f144b5d7cd2d4b4a5 --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/add_export.ts @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2025 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 * as ts from 'typescript'; + +import { isNonInterop } from './utils'; + +// add export for interface/class/enum/type/namespace, delete struct +export default function processVisitEachChild(context: ts.TransformationContext, node: ts.SourceFile, + exportFlag: boolean): ts.SourceFile { + return ts.visitEachChild(node, processAllNodes, context); + + function processAllNodes(node: ts.Node): ts.Node { + if (ts.isInterfaceDeclaration(node)) { + node = processInterfaceDeclaration(node, exportFlag); + } else if (ts.isClassDeclaration(node)) { + node = processClassDeclaration(node, exportFlag); + } else if (ts.isModuleDeclaration(node) && node.body && ts.isModuleBlock(node.body)) { + const newModuleBody: ts.ModuleBlock = + ts.factory.updateModuleBlock(node.body, getNewStatements(node)); + node = ts.factory.updateModuleDeclaration(node, node.modifiers, node.name, newModuleBody); + } else if (ts.isEnumDeclaration(node)) { + node = processEnumDeclaration(node, exportFlag); + } else if (ts.isStructDeclaration(node)) { + node = processStructDeclaration(node); + } else if (ts.isTypeAliasDeclaration(node)) { + node = processTypeAliasDeclaration(node, exportFlag); + } + return ts.visitEachChild(node, processAllNodes, context); + } +} + +function processInterfaceDeclaration(node: ts.InterfaceDeclaration, + exportFlag: boolean): ts.InterfaceDeclaration { + const newMembers: ts.TypeElement[] = []; + node.members.forEach((member) => { + if (!isNonInterop(member)) { + newMembers.push(member); + } + }); + let modifiers = exportFlag ? addExport2Modifiers(node.modifiers!) : node.modifiers; + return ts.factory.updateInterfaceDeclaration( + node, + modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + newMembers + ); +} + +function processClassDeclaration(node: ts.ClassDeclaration, exportFlag: boolean): ts.ClassDeclaration { + const newMembers: ts.ClassElement[] = []; + node.members.forEach((member) => { + if (!isNonInterop(member)) { + newMembers.push(member); + } + }); + let modifiers = exportFlag ? addExport2Modifiers(node.modifiers!) : node.modifiers; + return ts.factory.updateClassDeclaration( + node, + modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + newMembers + ); +} + +function processEnumDeclaration(node: ts.EnumDeclaration, exportFlag: boolean): ts.EnumDeclaration { + const newMembers: ts.EnumMember[] = []; + node.members.forEach((member) => { + if (!isNonInterop(member)) { + newMembers.push(member); + } + }); + let modifiers = exportFlag ? addExport2Modifiers(node.modifiers!) : node.modifiers; + return ts.factory.updateEnumDeclaration( + node, + modifiers, + node.name, + newMembers + ); +} + +function processStructDeclaration(node: ts.StructDeclaration): ts.StructDeclaration { + const newMembers: ts.ClassElement[] = []; + node.members.forEach((member, index) => { + if (index >= 1 && !isNonInterop(member)) { + newMembers.push(member); + } + }); + node = ts.factory.updateStructDeclaration( + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + newMembers + ); + return node; +} + +function processTypeAliasDeclaration(node: ts.TypeAliasDeclaration, + exportFlag: boolean): ts.TypeAliasDeclaration { + if (exportFlag) { + return ts.factory.updateTypeAliasDeclaration( + node, + addExport2Modifiers(node.modifiers!), + node.name, + node.typeParameters, + node.type + ); + } else { + return node; + } +} + +function getNewStatements(node: ts.ModuleDeclaration): ts.Statement[] { + const newStatements: ts.Statement[] = []; + (node.body! as ts.ModuleBlock).statements.forEach((statement) => { + if (!isNonInterop(statement)) { + newStatements.push(statement); + } + }); + return newStatements; +} + +function addExport2Modifiers( + modifiers: ts.NodeArray): ts.NodeArray { + modifiers = modifiers || []; + const isAlreadyExported = modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword); + if (!isAlreadyExported) { + modifiers = [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword), + ...modifiers] as unknown as ts.NodeArray; + } + return modifiers as ts.NodeArray; +} diff --git a/arkui_noninterop_plugin/src/delete_noninterop/delete_noninterop_api.ts b/arkui_noninterop_plugin/src/delete_noninterop/delete_noninterop_api.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6c032c5de2cfd910a6384b75abe2bf796fa5f9e --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/delete_noninterop_api.ts @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; + +import * as ts from 'typescript'; + +import { + ARKUI, + COMPONENT, + EXTNAME_TS, + OHOS_ARKUI, +} from './pre_define'; +import { whiteFileList } from './white_management'; +import { + getFileAndKitComment, + getPureName, + isExistImportFile, + isNonInterop, + processFileName, + processFileNameWithoutExt, + } from './utils'; +import type { + ExportStatementType, + FormatNodeInfo, + NeedDeleteExportInfo, + ProcessSourceFileResult, + } from './type'; +import { componentEtsFiles } from './global_var'; + +import processVisitEachChild from './add_export'; +import formatAllNodes from './format_import'; +import outputFile from './output_file'; + +let sourceFile: ts.SourceFile | null = null; +let componentEtsDeleteFiles: string[] = []; +const kitFileNeedDeleteMap = new Map(); + +export function deleteNonInteropApi(url: string, exportFlag: boolean, inputDir: string, + outputPath: string): ts.TransformerFactory { + return (context: ts.TransformationContext) => { + return (node: ts.SourceFile): ts.SourceFile => { + const fullText: string = String(node.getFullText()); + let fileAndKitComment: string = getFileAndKitComment(fullText); + const copyrightMessage: string = fullText.replace(node.getText(), '').split(/\/\*\*/)[0] + + fileAndKitComment + '\n'; + let kitName: string = ''; + if (fullText.match(/\@kit (.*)\r?\n/g)) { + kitName = RegExp.$1.replace(/\s/g, ''); + } + const fileName: string = processFileName(url); + sourceFile = node; + + const deleteNode: ProcessSourceFileResult = processSourceFile(node, url); + + node = processVisitEachChild(context, deleteNode.node, exportFlag); + + if (needProcessLabelNonInterop(fileName, kitName, inputDir)) { + const printer: ts.Printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const result: string = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); + + ts.transpileModule(result, { + compilerOptions: { + target: ts.ScriptTarget.ES2017, + }, + fileName: fileName, + transformers: { + before: [processDeleteNoninterop(url, exportFlag, inputDir, outputPath, + copyrightMessage, deleteNode.isCopyrightDeleted)] + }, + }); + } + return ts.factory.createSourceFile([], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None); + }; + }; +} + +function processSourceFile(node: ts.SourceFile, url: string): ProcessSourceFileResult { + let isCopyrightDeleted = false; + const newStatements: ts.Statement[] = []; + const newStatementsWithoutExport: ts.Statement[] = []; + const deleteNonInteropApiSet: Set = new Set(); + let needDeleteExport: NeedDeleteExportInfo = { + fileName: '', + default: '', + exportName: new Set(), + }; + isCopyrightDeleted = addNewStatements(node, newStatements, deleteNonInteropApiSet, needDeleteExport); + newStatements.forEach((statement) => { + const names = getExportIdentifierName(statement); + if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && + ts.isStringLiteral(statement.moduleSpecifier) && + statement.moduleSpecifier.text.startsWith(`./${ARKUI}/${COMPONENT}/`)) { + const importPath = statement.moduleSpecifier.text.replace(`./${ARKUI}/${COMPONENT}/`, ''); + const isDeleteSystemFile = componentEtsDeleteFiles.includes(getPureName(importPath)); + const hasEtsFile = componentEtsFiles.includes(getPureName(importPath)); + const existFile = isExistImportFile(path.dirname(url), statement.moduleSpecifier.text.toString()); + if (isDeleteSystemFile || !hasEtsFile && !existFile) { + return; + } + } + if (names.length === 0) { + newStatementsWithoutExport.push(statement); + return; + } + if (names.length === 1 && !deleteNonInteropApiSet.has(names[0])) { + newStatementsWithoutExport.push(statement); + return; + } + processExportNode(statement, node, needDeleteExport, names, deleteNonInteropApiSet, + newStatementsWithoutExport); + }); + if (needDeleteExport.fileName !== '') { + kitFileNeedDeleteMap.set(needDeleteExport.fileName, needDeleteExport); + } + return { + node: ts.factory.updateSourceFile(node, newStatementsWithoutExport, node.isDeclarationFile, + node.referencedFiles), + isCopyrightDeleted, + }; +} + +function needProcessLabelNonInterop(fileName: string, kitName: string, inputDir: string): boolean { + if (inputDir.endsWith(COMPONENT) || fileName.startsWith(OHOS_ARKUI) || + kitName.toLowerCase() === ARKUI || whiteFileList.includes(fileName.slice(0, -EXTNAME_TS.length))) { + return true; + } + return false; +} + +function processDeleteNoninterop(url: string, exportFlag: boolean, inputDir: string, + outputPath: string, copyrightMessage = '', isCopyrightDeleted = false) { + return (context: ts.TransformationContext) => { + return (node: ts.SourceFile): ts.SourceFile => { + sourceFile = node; + const allIdentifierSet: Set = collectAllIdentifier(node, context); + const formatValue: FormatNodeInfo = formatAllNodes(url, inputDir, node, allIdentifierSet); + node = formatValue.node; + const referencesMessage = formatValue.referencesMessage; + if (formatValue.isCopyrightDeleted) { + copyrightMessage = formatValue.copyrightMessage; + isCopyrightDeleted = formatValue.isCopyrightDeleted; + } + outputFile(url, exportFlag, inputDir, outputPath, node, sourceFile, referencesMessage, + copyrightMessage, isCopyrightDeleted); + return ts.factory.createSourceFile([], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None); + }; + }; +} + +function collectAllIdentifier(node: ts.SourceFile, context: ts.TransformationContext): Set { + const identifierSet: Set = new Set([]); + if (!ts.isSourceFile(node) || !node.statements) { + return identifierSet; + } + node.statements.forEach((stat) => { + if (!ts.isImportDeclaration(stat)) { + ts.visitEachChild(stat, collectAllNodes, context); + } + }); + + function collectAllNodes(node: ts.Node): ts.Node { + if (ts.isIdentifier(node)) { + identifierSet.add(node.escapedText.toString()); + } + return ts.visitEachChild(node, collectAllNodes, context); + } + + return identifierSet; +} + +function addNewStatements(node: ts.SourceFile, newStatements: ts.Statement[], + deleteNonInteropApiSet: Set, needDeleteExport: NeedDeleteExportInfo): boolean { + let isCopyrightDeleted = false; + node.statements.forEach((statement, index) => { + if (!isNonInterop(statement)) { + newStatements.push(statement); + return; + } + if (index === 0) { + isCopyrightDeleted = true; + } + if (ts.isVariableStatement(statement)) { + deleteNonInteropApiSet.add(variableStatementGetEscapedText(statement)); + } else if ( + ts.isModuleDeclaration(statement) || + ts.isInterfaceDeclaration(statement) || + ts.isClassDeclaration(statement) || + ts.isEnumDeclaration(statement) || + ts.isStructDeclaration(statement) || + ts.isTypeAliasDeclaration(statement) + ) { + if (statement && statement.name && (statement.name as ts.Identifier).escapedText) { + deleteNonInteropApiSet.add((statement.name as ts.Identifier).escapedText.toString()); + } + setDeleteExport(statement, node, needDeleteExport, deleteNonInteropApiSet); + } else if (ts.isExportAssignment(statement) || ts.isExportDeclaration(statement)) { + setDeleteExport(statement, node, needDeleteExport, deleteNonInteropApiSet); + } + }); + + return isCopyrightDeleted; +} + +function variableStatementGetEscapedText(statement: ts.VariableStatement): string { + let name = ''; + if ( + statement && + statement.declarationList && + statement.declarationList.declarations && + statement.declarationList.declarations.length > 0 && + statement.declarationList.declarations[0].name && + (statement.declarationList.declarations[0].name as ts.Identifier).escapedText + ) { + name = (statement.declarationList.declarations[0].name as ts.Identifier).escapedText.toString(); + } + return name; +} + +function processExportNode(statement: ts.Statement, node: ts.SourceFile, + needDeleteExport: NeedDeleteExportInfo, names: string[], deleteNonInteropApiSet: Set, + newStatementsWithoutExport: ts.Statement[]): void { + if (ts.isExportAssignment(statement)) { + needDeleteExport.fileName = processFileNameWithoutExt(node.fileName); + needDeleteExport.default = (statement.expression as ts.Identifier).escapedText.toString(); + } else if (ts.isExportDeclaration(statement)) { + let needExport = false; + const newSpecifiers: ts.ExportSpecifier[] = []; + names.forEach((name, index) => { + const exportSpecifier: ts.ExportSpecifier = + (statement.exportClause! as ts.NamedExports).elements![index]; + if (!deleteNonInteropApiSet.has(name)) { + newSpecifiers.push(exportSpecifier); + needExport = true; + } else { + needDeleteExport.fileName = processFileNameWithoutExt(node.fileName); + needDeleteExport.exportName.add(exportSpecifier.name.escapedText.toString()); + } + }); + if (needExport) { + (statement.exportClause as ts.NamedExports) = ts.factory.updateNamedExports( + statement.exportClause as ts.NamedExports, newSpecifiers); + newStatementsWithoutExport.push(statement); + } + } +} + +function setDeleteExport(statement: ts.Statement, node: ts.SourceFile, needDeleteExport: NeedDeleteExportInfo, + deleteNonInteropApiSet: Set): void { + if (ts.isExportAssignment(statement) && + deleteNonInteropApiSet.has((statement.expression as ts.Identifier).escapedText.toString())) { + needDeleteExport.fileName = processFileNameWithoutExt(node.fileName); + needDeleteExport.default = (statement.expression as ts.Identifier).escapedText.toString(); + } else if (ts.isExportDeclaration(statement)) { + needDeleteExport.fileName = processFileNameWithoutExt(node.fileName); + (statement.exportClause! as ts.NamedExports).elements.forEach((element) => { + const exportName = element.propertyName ? + element.propertyName.escapedText.toString() : + element.name.escapedText.toString(); + if (deleteNonInteropApiSet.has(exportName)) { + needDeleteExport.exportName.add(element.name.escapedText.toString()); + } + }); + } + //export namespace xxx {} + const modifiers = statement.modifiers; + if (modifiers === undefined) { + return; + } + const exportFlag = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword); + const defaultFlag = modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword); + if (exportFlag && defaultFlag) { + needDeleteExport.fileName = processFileNameWithoutExt(node.fileName); + needDeleteExport.default = ((statement! as ExportStatementType)!.name! as ts.Identifier) + .escapedText.toString(); + } else if (exportFlag) { + needDeleteExport.fileName = processFileNameWithoutExt(node.fileName); + needDeleteExport.exportName.add(((statement! as ExportStatementType)!.name! as ts.Identifier) + .escapedText.toString()); + } +} + +function getExportIdentifierName(statement: ts.Statement): string[] { + const names = []; + if (ts.isExpressionStatement(statement)) { + // exports.name = xxx; + if (ts.isBinaryExpression(statement.expression) && ts.isIdentifier(statement.expression.right) && + statement.expression.right.escapedText) { + names.push(statement.expression.right.escapedText.toString()); + } + } else if (ts.isExportAssignment(statement)) { + // export default xxx + names.push((statement.expression as ts.Identifier).escapedText.toString()); + } else if (ts.isExportDeclaration(statement) && statement.exportClause) { + // export {xxx} 、export {xxx as yyy} 、export * from './zzz' + const specifiers = (statement.exportClause as ts.NamedExports).elements; + specifiers.forEach((specifier) => { + if (ts.isExportSpecifier(specifier)) { + const name = specifier.propertyName ? specifier.propertyName : specifier.name; + names.push(name.escapedText.toString()); + } + }); + } + return names; +} diff --git a/arkui_noninterop_plugin/src/delete_noninterop/format_import.ts b/arkui_noninterop_plugin/src/delete_noninterop/format_import.ts new file mode 100644 index 0000000000000000000000000000000000000000..43043f6177da90f845f09d1d79bd55acdd90d339 --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/format_import.ts @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; + +import * as ts from 'typescript'; + +import { + ARKUI, + ARKTS, + COMPONENT, + ANY, + ETS, + INTERNAL, + EXIST, + NON_EXIST, + FRAMENODE, + TYPENODE, + XCOMPONENT, +} from './pre_define'; +import { + getCoreFilename, + getPureName, + isExistImportFile, + } from './utils'; +import type { + ClauseSetValueInfo, + FormatImportInfo, + FormatNodeInfo, + ReferenceModuleInfo, + } from './type'; +import { componentEtsFiles } from './global_var'; + +const globalModules = new Map(); + +export default function formatAllNodes(url: string, inputDir: string, node: ts.SourceFile, + allIdentifierSet: Set, copyrightMessage = '', + isCopyrightDeleted = false): FormatNodeInfo { + let referencesMessage: string = ''; + let currReferencesModule: ReferenceModuleInfo[] = []; + if (!ts.isSourceFile(node) || !node.statements) { + return { node, referencesMessage, copyrightMessage, isCopyrightDeleted }; + } + const newStatements: ts.Statement[] = []; + node.statements.forEach((statement) => { + if (ts.isImportDeclaration(statement)) { + const importInfo: FormatImportInfo = formatAllNodesImportDeclaration(node, statement, url, + inputDir, currReferencesModule, allIdentifierSet); + if (importInfo.statement) { + newStatements.push(statement); + } else if (importInfo.isCopyrightDeleted) { + copyrightMessage = importInfo.copyrightMessage!; + isCopyrightDeleted = importInfo.isCopyrightDeleted; + } + } else if (ts.isStructDeclaration(statement)) { + statement = ts.factory.updateStructDeclaration(statement, statement.modifiers, statement.name, + statement.typeParameters, statement.heritageClauses, statement.members.slice(1)); + newStatements.push(statement); + } else { + newStatements.push(statement); + } + }); + + addForSpecialFiles(node, newStatements); + + currReferencesModule.forEach((item) => { + if (item.isUsed) { + referencesMessage += item.reference + '\n'; + } + }); + node = ts.factory.updateSourceFile(node, newStatements); + return { node, referencesMessage, copyrightMessage, isCopyrightDeleted }; +} + +function formatAllNodesImportDeclaration(node: ts.SourceFile, statement: ts.ImportDeclaration, + url: string, inputDir: string, currReferencesModule: ReferenceModuleInfo[], + allIdentifierSet: Set): FormatImportInfo { + const clauseSet: Set = getClauseSet(statement); + const importSpecifier: string = statement.moduleSpecifier.getText().replace(/[\'\"]/g, ''); + const fileDir: string = path.dirname(url); + const hasImportSpecifierFile: boolean = hasFileByImportPath(importSpecifier, fileDir, inputDir); + let hasImportSpecifierInModules: boolean = globalModules.has(importSpecifier); + if ((!hasImportSpecifierFile && !hasImportSpecifierInModules) || clauseSet.size === 0) { + if (hasCopyright(statement)) { + return { copyrightMessage: node.getFullText().replace(node.getText(), ''), isCopyrightDeleted: true }; + } else { + return { statement: undefined, copyrightMessage: '', isCopyrightDeleted: false }; + } + } + const clauseSetValue: ClauseSetValueInfo = getExsitClauseSet(hasImportSpecifierInModules, importSpecifier, + currReferencesModule, clauseSet, allIdentifierSet); + const hasExsitStatus: boolean = clauseSetValue.hasExsitStatus; + const hasNonExsitStatus: boolean = clauseSetValue.hasNonExsitStatus; + const exsitClauseSet: Set = clauseSetValue.exsitClauseSet; + if (hasExsitStatus) { + return handleUsedImport(hasNonExsitStatus, statement, exsitClauseSet, + hasImportSpecifierInModules, currReferencesModule, importSpecifier); + } else if (hasCopyright(statement)) { + return { copyrightMessage: node.getFullText().replace(node.getText(), ''), isCopyrightDeleted: true }; + } else { + return { statement: undefined, copyrightMessage: '', isCopyrightDeleted: false }; + } +} + +function addForSpecialFiles(node: ts.SourceFile, newStatements: ts.Statement[]): void { + const fileName = getCoreFilename(path.basename(node.fileName)); + if (fileName === FRAMENODE) { + newStatements.push(createFrameNodeTypeNode()); + } +} + +function hasFileByImportPath(importPath: string, apiDir: string, inputDir: string): boolean { + let fileDir: string = path.resolve(apiDir); + if (importPath.startsWith(`@${ARKTS}`)) { + fileDir = path.resolve(inputDir, `../${ARKTS}`); + } + return isExistArkUIFile(path.resolve(inputDir, ARKUI, COMPONENT), importPath, inputDir) || + isExistImportFile(fileDir, importPath); +} + +function isExistArkUIFile(resolvedPath: string, importPath: string, inputDir: string): boolean { + const filePath = path.resolve(resolvedPath, importPath); + if (filePath.includes(path.resolve(inputDir, INTERNAL, COMPONENT, ETS)) || + filePath.includes(path.resolve(inputDir, ARKUI, COMPONENT)) + ) { + const fileName = getPureName(filePath); + return componentEtsFiles.includes(fileName); + } + return isExistImportFile(resolvedPath, importPath); +} + +function createFrameNodeTypeNode(): ts.ModuleDeclaration { + return ts.factory.createModuleDeclaration( + [ + ts.factory.createToken(ts.SyntaxKind.ExportKeyword), + ts.factory.createToken(ts.SyntaxKind.DeclareKeyword) + ], + ts.factory.createIdentifier(TYPENODE), + ts.factory.createModuleBlock([ts.factory.createTypeAliasDeclaration( + undefined, + ts.factory.createIdentifier(XCOMPONENT), + undefined, + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier(ANY), + undefined + ) + )]), + ts.NodeFlags.Namespace | ts.NodeFlags.ExportContext | ts.NodeFlags.ContextFlags + ); +} + +function hasCopyright(node: ts.ImportDeclaration): boolean { + return /http\:\/\/www\.apache\.org\/licenses\/LICENSE\-2\.0/g.test(node.getFullText() + .replace(node.getText(), '')); +} + +function getClauseSet(statement: ts.ImportDeclaration): Set { + const clauseSet: Set = new Set([]); + if (!statement.importClause || !ts.isImportClause(statement.importClause)) { + return clauseSet; + } + const clauseNode: ts.ImportClause = statement.importClause; + if (!clauseNode.namedBindings && clauseNode.name && ts.isIdentifier(clauseNode.name)) { + clauseSet.add(clauseNode.name.escapedText.toString()); + } else if (clauseNode.namedBindings && ts.isNamespaceImport(clauseNode.namedBindings) && + clauseNode.namedBindings.name && ts.isIdentifier(clauseNode.namedBindings.name)) { + clauseSet.add(clauseNode.namedBindings.name.escapedText.toString()); + } else if (clauseNode.namedBindings && ts.isNamedImports(clauseNode.namedBindings) && + clauseNode.namedBindings.elements) { + clauseNode.namedBindings.elements.forEach((ele) => { + if (ele.name && ts.isIdentifier(ele.name)) { + clauseSet.add(ele.name.escapedText.toString()); + } + }); + } + return clauseSet; +} + +function getExsitClauseSet(hasImportSpecifierInModules: boolean, importSpecifier: string, + currReferencesModule: ReferenceModuleInfo[], clauseSet: Set, + allIdentifierSet: Set): ClauseSetValueInfo { + let currModule: string[] = []; + if (hasImportSpecifierInModules) { + let index: number = globalModules.get(importSpecifier); + const referenceModule: ReferenceModuleInfo = currReferencesModule[index]; + currModule = referenceModule.modules[importSpecifier]!; + } + const clasueCheckList = []; + let exsitClauseSet: Set = new Set([]); + for (const clause of clauseSet) { + let flag = allIdentifierSet.has(clause); + if (hasImportSpecifierInModules) { + flag = allIdentifierSet.has(clause) && currModule.includes(clause); + } + if (flag) { + // use import + exsitClauseSet.add(clause); + clasueCheckList.push(EXIST); + } else { + clasueCheckList.push(NON_EXIST); + } + } + let hasExsitStatus = false; + let hasNonExsitStatus = false; + clasueCheckList.forEach((ele) => { + if (ele === EXIST) { + hasExsitStatus = true; + } else { + hasNonExsitStatus = true; + } + }); + return { exsitClauseSet, hasExsitStatus, hasNonExsitStatus }; +} + +function handleUsedImport(hasNonExsitStatus: boolean, statement: ts.ImportDeclaration, + exsitClauseSet: Set, hasImportSpecifierInModules: boolean, + currReferencesModule: ReferenceModuleInfo[], importSpecifier: string): FormatImportInfo { + if (hasNonExsitStatus) { + const newSpecifiers: ts.ImportSpecifier[] = []; + (statement.importClause!.namedBindings! as ts.NamedImports).elements.forEach((element) => { + if (exsitClauseSet.has(element.name.escapedText.toString())) { + newSpecifiers.push(element); + } + }); + if (statement.importClause && ts.isNamedImports(statement.importClause.namedBindings!)) { + // @ts-ignore + statement.importClause.namedBindings = ts.factory.updateNamedImports( + statement.importClause.namedBindings, + newSpecifiers + ); + } + } + if (hasImportSpecifierInModules) { + let index = globalModules.get(importSpecifier); + currReferencesModule[index].isUsed = true; + } + return { statement }; +} diff --git a/arkui_noninterop_plugin/src/delete_noninterop/global_var.ts b/arkui_noninterop_plugin/src/delete_noninterop/global_var.ts new file mode 100644 index 0000000000000000000000000000000000000000..def2cc05094e55859683e7b942d1674d39aecf7a --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/global_var.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 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 const stmtReplacementMap = new Map(); +export const componentEtsFiles: string[] = []; diff --git a/arkui_noninterop_plugin/src/delete_noninterop/index.ts b/arkui_noninterop_plugin/src/delete_noninterop/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea0f9905dab4f47b514473ce4ae7f7563edd9bcb --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2025 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 start from './start'; + +start(); diff --git a/arkui_noninterop_plugin/src/delete_noninterop/output_file.ts b/arkui_noninterop_plugin/src/delete_noninterop/output_file.ts new file mode 100644 index 0000000000000000000000000000000000000000..53defd9e461bfc96d498d23c21f4f32492bf8c92 --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/output_file.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +import * as ts from 'typescript'; + +import { GLOBAL_ESVALUE_FILE } from './pre_define'; +import { + removeNonInteropDoc, + removeComments, + writeFile, + } from './utils'; + +import { stmtReplacementMap } from './global_var'; + +export default function outputFile(url: string, exportFlag: boolean, inputDir: string, + outputPath: string, node: ts.SourceFile, sourceFile: ts.SourceFile, referencesMessage: string, + copyrightMessage: string, isCopyrightDeleted: boolean): void { + const printer: ts.Printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + let result: string = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); + if (isCopyrightDeleted) { + result = copyrightMessage + '\n' + result; + } + copyrightMessage = node.getFullText().replace(node.getText(), ''); + if (referencesMessage) { + result = result.substring(0, copyrightMessage.length) + '\n' + referencesMessage + + result.substring(copyrightMessage.length); + } + result = removeNonInteropDoc(result); + result = postProcessContent(result); + writeFile(url, result, inputDir, outputPath); + // api in component need merge + if (exportFlag) { + writeGlobalESValueFile(removeComments(result), outputPath); + } +} + +function postProcessContent(content: string): string { + for (const [originalStmt, transformedStmt] of stmtReplacementMap) { + content = content.replace(transformedStmt, originalStmt); + } + return content.replace(/^(\s*)\/\*\*\@reserved (.*) \*\/$/mg, '$1$2'); +} + +function writeGlobalESValueFile(content: string, outputPath: string): void { + content = content.replace("'use static';", '').replace(/\.\.\/api/g, '.'); + fs.appendFileSync(`${path.resolve(outputPath, '../api')}/${GLOBAL_ESVALUE_FILE}`, content); +} diff --git a/arkui_noninterop_plugin/src/delete_noninterop/pre_define.ts b/arkui_noninterop_plugin/src/delete_noninterop/pre_define.ts new file mode 100644 index 0000000000000000000000000000000000000000..80e433bd395f54e7fb641628609cc487a61f3c92 --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/pre_define.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 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 const GLOBAL_ESVALUE_FILE: string = '@ohos.arkui.GlobalESValue.d.ts'; +export const ARKUI: string = 'arkui'; +export const ARKTS: string = 'arkts'; +export const COMPONENT: string = 'component'; + +export const ANY: string = 'Any'; +export const EXTNAME_D_TS: string = '.d.ts'; +export const EXTNAME_D_ETS: string = '.d.ets'; +export const EXTNAME_TS: string = '.ts'; +export const EXTNAME_ETS: string = '.ets'; +export const ETS: string = 'ets'; +export const INTERNAL: string = '@internal'; +export const EXIST: string = 'exist'; +export const NON_EXIST: string = 'non-exist'; +export const OHOS_ARKUI: string = '@ohos.arkui'; + +export const FRAMENODE: string = 'FrameNode'; +export const TRUE: string = 'true'; +export const TYPENODE: string = 'typeNode'; +export const XCOMPONENT: string = 'XComponent'; + +export const ALERT_DIALOG: string = 'alert_dialog'; +export const ALERT_DIALOG_TEXT_STYLE: string = 'AlertDialogTextStyle'; +export const COMMON: string = 'common'; +export const COMMON_LINEAR_GRADIENT: string = 'CommonLinearGradient'; diff --git a/arkui_noninterop_plugin/src/delete_noninterop/process_label_noninterop.ts b/arkui_noninterop_plugin/src/delete_noninterop/process_label_noninterop.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4c64e7875433d949b4d18dcaf1b370ceab57dd9 --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/process_label_noninterop.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +import * as ts from 'typescript'; + +import { + processFileName, + writeFile, + } from './utils'; +import { stmtReplacementMap } from './global_var'; +import { + isSpecialFile, + processSpecialFileContext, +} from './process_special_file'; + +export function tsTransform(utFiles: string[], callback: Function, exportFlag: boolean, + inputDir: string, outputPath: string): void { + utFiles.forEach((url) => { + const apiBaseName = path.basename(url); + let content = fs.readFileSync(url, 'utf-8'); + let isTransformer = /\.d\.ts/.test(apiBaseName) || /\.d\.ets/.test(apiBaseName); + if (/\.json/.test(url)) { + isTransformer = false; + } + if (!isTransformer) { + writeFile(url, content, inputDir, outputPath); + return; + } + const fileName = processFileName(url); + ts.transpileModule(preprocessContent(fileName, content), { + compilerOptions: { + target: ts.ScriptTarget.ES2017, + }, + fileName: fileName, + transformers: { before: [callback(url, exportFlag, inputDir, outputPath)] }, + }); + }); +} + +function preprocessContent(fileName: string, content: string): string { + stmtReplacementMap.clear(); + let result = content.replace(/^(\s*)(\@Retention\(\{[^\(\)\{\}]*\}\)$)/mg, + '$1/**@reserved $2 */'); + const matches = result.match(/(^[^\*]*\s+\@interface\s+.*$)/mg); + if (matches) { + for (const match of matches) { + const transformedStmt: string = match.replace(/(?<=\s+)\@interface(\s+\w+)\s*\{\}/g, 'const$1'); + result = result.replace(match, transformedStmt); + stmtReplacementMap.set(match, transformedStmt); + } + } + + if (isSpecialFile(fileName)) { + result = processSpecialFileContext(fileName, result); + } + return result; +} diff --git a/arkui_noninterop_plugin/src/delete_noninterop/process_special_file.ts b/arkui_noninterop_plugin/src/delete_noninterop/process_special_file.ts new file mode 100644 index 0000000000000000000000000000000000000000..9705f33960c1a6347ed407a57eead80e4cc9c78f --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/process_special_file.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; + +import { + ALERT_DIALOG, + ALERT_DIALOG_TEXT_STYLE, + COMMON, + COMMON_LINEAR_GRADIENT, + EXTNAME_TS, +} from './pre_define'; + +import { specialFileList } from './white_management'; + +function processSpecialFileContext(fileName: string, context: string): string { + fileName = path.basename(fileName, EXTNAME_TS); + if (fileName === ALERT_DIALOG) { + context = context.replace(/\bTextStyle\b/g, ALERT_DIALOG_TEXT_STYLE); + } + if (fileName === COMMON) { + context = context.replace(/\bLinearGradient\b/g, COMMON_LINEAR_GRADIENT); + } + return context; +} + +function isSpecialFile(url: string): boolean { + return specialFileList.includes(path.basename(url, EXTNAME_TS)); +} + +export { + isSpecialFile, + processSpecialFileContext, +}; diff --git a/arkui_noninterop_plugin/src/delete_noninterop/start.ts b/arkui_noninterop_plugin/src/delete_noninterop/start.ts new file mode 100644 index 0000000000000000000000000000000000000000..cba2f0d11284f98c544924111951434d8dd4d525 --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/start.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 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 * as commander from 'commander'; + +import { TRUE } from './pre_define'; +import { transformFiles } from './transform_plugin'; + +let outputPath: string = ''; +let inputDir: string = ''; +let exportFlag: boolean = false; + +export default function start(): void { + const program = new commander.Command(); + program + .name('noninterop') + .version('0.0.1'); + program + .option('--input ', 'input path') + .option('--output ', 'output path') + .option('--export ', 'export flag', false) + .action((opts) => { + outputPath = opts.output; + inputDir = opts.input; + exportFlag = opts.export === TRUE; + transformFiles(inputDir, outputPath, exportFlag); + }); + program.parse(process.argv); +} diff --git a/arkui_noninterop_plugin/src/delete_noninterop/transform_plugin.ts b/arkui_noninterop_plugin/src/delete_noninterop/transform_plugin.ts new file mode 100644 index 0000000000000000000000000000000000000000..c23e49e25a5507b0d62e228cb912726c3f460233 --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/transform_plugin.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +import { GLOBAL_ESVALUE_FILE } from './pre_define'; +import { readFile } from './utils'; + +import { tsTransform } from './process_label_noninterop'; +import { deleteNonInteropApi } from './delete_noninterop_api'; + +export function transformFiles(inputDir: string, outputPath: string, exportFlag: boolean): void { + try { + if (exportFlag) { + initGlobalESValueFile(outputPath); + } + const utFiles: string[] = []; + readFile(inputDir, utFiles); + tsTransform(utFiles, deleteNonInteropApi, exportFlag, inputDir, outputPath); + } catch (error) { + // ignore + } +} + +function initGlobalESValueFile(outputPath: string): void { + const filePath: string = `${path.resolve(outputPath, '../api')}/${GLOBAL_ESVALUE_FILE}`; + const dir: string = path.dirname(filePath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(filePath, ''); +} diff --git a/arkui_noninterop_plugin/src/delete_noninterop/type.ts b/arkui_noninterop_plugin/src/delete_noninterop/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..240a0313c801d64e0165bd90da995614511fd078 --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/type.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 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 * as ts from 'typescript'; + +interface ProcessSourceFileResult { + node: ts.SourceFile; + isCopyrightDeleted: boolean; +} + +interface NeedDeleteExportInfo { + fileName: string; + default: string; + exportName: Set; +} + +interface ReferenceModuleInfo { + isUsed: boolean; + modules: Record; + reference?: string; +} + +interface FormatNodeInfo { + node: ts.SourceFile; + referencesMessage: string; + copyrightMessage: string; + isCopyrightDeleted: boolean; +} + +interface FormatImportInfo { + statement?: ts.ImportDeclaration; + copyrightMessage?: string; + isCopyrightDeleted?: boolean; +} + +interface ClauseSetValueInfo { + exsitClauseSet: Set; + hasExsitStatus: boolean; + hasNonExsitStatus: boolean; +} + +type ExportStatementType = ts.ModuleDeclaration | + ts.InterfaceDeclaration | + ts.ClassDeclaration | + ts.EnumDeclaration | + ts.StructDeclaration | + ts.TypeAliasDeclaration; + +export type { + ClauseSetValueInfo, + ExportStatementType, + FormatImportInfo, + FormatNodeInfo, + NeedDeleteExportInfo, + ProcessSourceFileResult, + ReferenceModuleInfo, +}; diff --git a/arkui_noninterop_plugin/src/delete_noninterop/utils.ts b/arkui_noninterop_plugin/src/delete_noninterop/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..049f0e47892747a4cb91f7c23fa67c41358e7fda --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/utils.ts @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +import * as ts from 'typescript'; + +import { + EXTNAME_D_TS, + EXTNAME_D_ETS, + EXTNAME_TS, + EXTNAME_ETS, +} from './pre_define'; + +function readFile(dir: string, utFiles: string[]): void { + try { + const files = fs.readdirSync(dir); + files.forEach((element: string): void => { + const filePath = path.join(dir, element); + const status = fs.statSync(filePath); + if (status.isDirectory()) { + readFile(filePath, utFiles); + } else { + utFiles.push(filePath); + } + }); + } catch (e) { + // ignore + } +} + +function processFileName(filePath: string): string { + return path.basename(filePath) + .replace(/\.d\.ts$/g, EXTNAME_TS) + .replace(/\.d\.ets$/g, EXTNAME_ETS); +} + +function processFileNameWithoutExt(filePath: string): string { + return path.basename(filePath) + .replace(/\.d\.ts$/g, '') + .replace(/\.d\.ets$/g, '') + .replace(/\.ts$/g, '') + .replace(/\.ets$/g, ''); +} + +function getPureName(name: string): string { + return path.basename(name) + .replace(EXTNAME_D_TS, '') + .replace(EXTNAME_D_ETS, '') + .replace(/_/g, '') + .toLowerCase(); +} + +function isExistImportFile(fileDir: string, importPath: string): boolean { + return [EXTNAME_D_TS, EXTNAME_D_ETS].some(ext => { + return fs.existsSync(path.resolve(fileDir, `${importPath}${ext}`)); + }); +} + +function getCoreFilename(fileName: string): string { + if (fileName.endsWith(EXTNAME_TS)) { + return fileName.slice(0, -EXTNAME_TS.length); + } + return fileName; +} + +function getFileAndKitComment(fileFullText: string): string { + let fileAndKitComment: string = ''; + let pattern: RegExp = /\/\*\*\s*\*\s*@file[\s\S]*?@kit[\s\S]*?\*\//; + let comment: RegExpMatchArray | null = fileFullText.match(pattern); + if (comment) { + fileAndKitComment = comment[0]; + } + return fileAndKitComment; +} + +function removeNonInteropDoc(result: string): string { + return result.replace(/\/\*\*[\s\S]*?\*\//g, (substring: string): string => { + return /@noninterop/g.test(substring) ? '' : substring; + }); +} + +function writeFile(url: string, data: string, inputDir: string, outputPath: string): void { + const newFilePath: string = path.resolve(outputPath, path.relative(inputDir, url)); + fs.mkdirSync(path.dirname(newFilePath), { recursive: true }); + fs.writeFileSync(newFilePath, data); +} + +function removeComments(content: string): string { + return content + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\/\/.*$/gm, '') + .replace(/^\s*[\r\n]/gm, ''); +} + +function isNonInterop(node: ts.Node): boolean { + const notesContent: string = node.getFullText().replace(node.getText(), '').replace(/[\s]/g, ''); + const notesArr: string[] = notesContent.split(/\/\*\*/); + for (const note of notesArr) { + if (note.length !== 0 && /@noninterop/g.test(note)) { + return true; + } + } + return false; +} + +export { + getCoreFilename, + getFileAndKitComment, + getPureName, + isExistImportFile, + isNonInterop, + processFileName, + processFileNameWithoutExt, + readFile, + removeNonInteropDoc, + removeComments, + writeFile, +}; diff --git a/arkui_noninterop_plugin/src/delete_noninterop/white_management.ts b/arkui_noninterop_plugin/src/delete_noninterop/white_management.ts new file mode 100644 index 0000000000000000000000000000000000000000..e46a551a8818fd24d78682fdec42d69ac9140a2a --- /dev/null +++ b/arkui_noninterop_plugin/src/delete_noninterop/white_management.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 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 const whiteFileList: string[] = [ + 'web', +]; + +export const specialFileList: string[] = [ + 'alert_dialog', + 'common', +]; diff --git a/arkui_noninterop_plugin/test/config.ts b/arkui_noninterop_plugin/test/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..e022ce148921bb1e3340aef4f41a6a3670db869d --- /dev/null +++ b/arkui_noninterop_plugin/test/config.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; + +const GLOBAL_ESVALUE_FILE: string = '@ohos.arkui.GlobalESValue.d.ts'; +const API: string = 'api'; +const COMPONENT: string = 'component'; +const SOURCE: string = 'source'; +const BUILD: string = 'build_delete_noninterop'; +const UT: string = 'ut'; +const UTF_8: BufferEncoding = 'utf8'; + +const PROJECT_ROOT: string = path.resolve(__dirname, '../../test'); +const ADD_IMPORT_SOURCE_PATH: string = path.resolve(PROJECT_ROOT, 'ut', API); +const ADD_IMPORT_OUTPUTS_PATH: string = path.resolve(PROJECT_ROOT, 'build_add_import', API); +const ADD_IMPORT_TARGET_PATH: string = path.resolve(PROJECT_ROOT, 'target', API); + +const SAMPLE: string = 'sample'; +const SAMPLE_BUILD: string = 'build_sample'; +const SAMPLE_RESULT: string = 'build_sample_result'; + +const SAMPLE_API: string[] = [ + './api/@ohos.arkui.xxx.d.ts', + './component/yyy.d.ts', +]; + +export { + ADD_IMPORT_OUTPUTS_PATH, + ADD_IMPORT_SOURCE_PATH, + ADD_IMPORT_TARGET_PATH, + API, + BUILD, + COMPONENT, + GLOBAL_ESVALUE_FILE, + PROJECT_ROOT, + SAMPLE, + SAMPLE_API, + SAMPLE_BUILD, + SAMPLE_RESULT, + SOURCE, + UT, + UTF_8, +} diff --git a/arkui_noninterop_plugin/test/sample/api/@ohos.arkui.xxx.d.ts b/arkui_noninterop_plugin/test/sample/api/@ohos.arkui.xxx.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b9fea2d0afe1f794a208cd5b6e0bc9bbaab7efc --- /dev/null +++ b/arkui_noninterop_plugin/test/sample/api/@ohos.arkui.xxx.d.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 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. + */ +/** + * @file + * @kit ArkUI + */ + +/** + * xxx1. + * + * @interface Xxx1 + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @form + * @atomicservice + * @since 11 + * @noninterop + */ +export interface Xxx1 { +} + +/** + * xxx2. + * + * @interface Xxx2 + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @form + * @atomicservice + * @since 11 + */ +export interface Xxx2 { +} diff --git a/arkui_noninterop_plugin/test/sample/component/yyy.d.ts b/arkui_noninterop_plugin/test/sample/component/yyy.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..4fffb7f8cb6838943d1dabc3a20008548e518255 --- /dev/null +++ b/arkui_noninterop_plugin/test/sample/component/yyy.d.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 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. + */ +/** + * @file + * @kit ArkUI + */ + +/** + * yyy1. + * + * @interface Yyy1 + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @form + * @atomicservice + * @since 11 + */ +interface Yyy1 { +} + +/** + * xxx2. + * + * @interface Xxx2 + * @syscap SystemCapability.ArkUI.ArkUI.Full + * @form + * @atomicservice + * @since 11 + * @noninterop + */ +interface Yyy2 { +} diff --git a/arkui_noninterop_plugin/test/test.ts b/arkui_noninterop_plugin/test/test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b45cb7a65edffeff3798eda5b733742fafdb7a2 --- /dev/null +++ b/arkui_noninterop_plugin/test/test.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; +import * as mocha from 'mocha'; +import { expect } from 'chai'; + +import * as ts from 'typescript'; + +import { + ADD_IMPORT_OUTPUTS_PATH, + ADD_IMPORT_SOURCE_PATH, + ADD_IMPORT_TARGET_PATH, + API, + BUILD, + COMPONENT, + GLOBAL_ESVALUE_FILE, + PROJECT_ROOT, + SOURCE, + UT, + UTF_8, +} from './config'; +import { getFiles, parseCode } from './utils'; + +import { transformFiles } from '../src/delete_noninterop/transform_plugin'; +import processInteropUI from '../src/add_import/process_interop_ui'; + +mocha.describe('add import for 1.1 interop sdk', () => { + const deleteNoninteropOutputPath: string = path.resolve(PROJECT_ROOT, BUILD); + const sourceFilePath: string = path.resolve(PROJECT_ROOT, SOURCE); + + transformFiles(path.resolve(sourceFilePath, API), + path.resolve(deleteNoninteropOutputPath, API), false); + transformFiles(path.resolve(sourceFilePath, COMPONENT), + path.resolve(deleteNoninteropOutputPath, COMPONENT), true); + + const utFiles: string[] = []; + getFiles(sourceFilePath, utFiles); + utFiles.push(path.resolve(sourceFilePath, API, GLOBAL_ESVALUE_FILE)); + + utFiles.forEach((filePath: string, index: number) => { + const buildFilePath: string = path.resolve(filePath.replace(`/${SOURCE}/`, `/${BUILD}/`)); + const targetFilePath: string = path.resolve(filePath.replace(`/${SOURCE}/`, `/${UT}/`)); + + if (fs.existsSync(buildFilePath) && fs.existsSync(targetFilePath)) { + mocha.it(`${index + 1}: test delete noninterop ${path.basename(filePath)}`, function (done) { + const buildCode: string = fs.readFileSync(buildFilePath, UTF_8); + const targetCode: string = fs.readFileSync(targetFilePath, UTF_8); + expect(parseCode(buildCode)).eql(parseCode(targetCode)); + done(); + }); + } + }); + + const intputDir: string = ADD_IMPORT_SOURCE_PATH; + const outputDir: string = ADD_IMPORT_OUTPUTS_PATH; + const targetDir: string = ADD_IMPORT_TARGET_PATH; + const res: ts.TransformationResult = processInteropUI(intputDir, false, outputDir); + + res.transformed.map((sourcefile:ts.SourceFile, index: number) => { + const buildFilePath: string = path.resolve(sourcefile.fileName.replace(intputDir, outputDir)); + const targetFilePath: string = path.resolve(sourcefile.fileName.replace(intputDir, targetDir)); + + if (fs.existsSync(targetFilePath) && !targetFilePath.includes(`/${COMPONENT}/`)) { + mocha.it(`${index + 1}: test add import ${sourcefile.fileName.replace(intputDir, '')}`, + function (done) { + const buildCode: string = fs.readFileSync(buildFilePath, UTF_8); + const targetCode: string = fs.readFileSync(targetFilePath, UTF_8); + expect(parseCode(buildCode)).eql(parseCode(targetCode)); + done(); + }); + } + }); +}); diff --git a/arkui_noninterop_plugin/test/testSample.ts b/arkui_noninterop_plugin/test/testSample.ts new file mode 100644 index 0000000000000000000000000000000000000000..01eef77dd155942491b4bef84340c276889d274e --- /dev/null +++ b/arkui_noninterop_plugin/test/testSample.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; + +import { + API, + COMPONENT, + PROJECT_ROOT, + SAMPLE, + SAMPLE_BUILD, + SAMPLE_RESULT, +} from './config'; + +const { transformFiles } = require(path.resolve(PROJECT_ROOT, '../../process_label_noninterop')); +const { processInteropUI } = require(path.resolve(PROJECT_ROOT, '../../process_global_import')); + +const sourceFilePath: string = path.resolve(PROJECT_ROOT, SAMPLE); +const deleteNoninteropOutputPath: string = path.resolve(PROJECT_ROOT, SAMPLE_BUILD); +transformFiles(path.resolve(sourceFilePath, API), + path.resolve(deleteNoninteropOutputPath, API), false); +transformFiles(path.resolve(sourceFilePath, COMPONENT), + path.resolve(deleteNoninteropOutputPath, COMPONENT), true); + +const addExportIntputDir: string = deleteNoninteropOutputPath; +const outPath: string = path.resolve(PROJECT_ROOT, SAMPLE_RESULT); +processInteropUI(addExportIntputDir, false, outPath); diff --git a/arkui_noninterop_plugin/test/tsconfig.json b/arkui_noninterop_plugin/test/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..37d80913c73b1cc49410eba13346c4a2a348375c --- /dev/null +++ b/arkui_noninterop_plugin/test/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "outDir": "../dist-test", + "types": [ + "node", + "mocha", + "chai" + ] + }, + "include": [ + "./**/*.ts", + "../src/**/*.ts" + ], + "exclude": [ + "../node_modules", + "../dist", + "../dist-test", + "./build_delete_noninterop", + "./build_add_import", + "./build_sample", + "./build_sample_result", + "./source", + "./ut", + "./target", + "./sample" + ] +} diff --git a/arkui_noninterop_plugin/test/utils.ts b/arkui_noninterop_plugin/test/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..18ff76b49928b623fe7a7495c6a9f291ce16842b --- /dev/null +++ b/arkui_noninterop_plugin/test/utils.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 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 * as path from 'path'; +import * as fs from 'fs'; + +function parseCode(code: string): string { + return normalizeFileContent(cleanCopyRight(code)); +} + +function normalizeFileContent(content: string): string { + // Replace all types of line endings with a single newline character + const normalizedLineEndings = content.replace(/\r\n|\r/g, '\n'); + + // Remove leading and trailing whitespace from each line + const normalizedWhitespace = normalizedLineEndings.split('\n').map(line => line.trim()).join('\n'); + + // Remove empty lines + const normalizedEmptyLines = normalizedWhitespace.split('\n').filter(line => line !== '').join('\n'); + + return normalizedEmptyLines; +} + +function cleanCopyRight(str: string): string { + const copyrightBlockRegex = /(?:\/\*.*Copyright \([c|C]\) [- \d]+ [\w ]+\., Ltd\..*\*\/)/gs; + + return str.replace(copyrightBlockRegex, ''); +} + +function getFiles(dir: string, allFiles: string[] = []): void { + const files: string[] = fs.readdirSync(dir); + files.forEach((element) => { + const filePath = path.join(dir, element); + const status = fs.statSync(filePath); + if (status.isDirectory()) { + getFiles(filePath, allFiles); + } else { + allFiles.push(filePath); + } + }); +} + +export { + getFiles, + parseCode, +} diff --git a/arkui_noninterop_plugin/tsconfig.json b/arkui_noninterop_plugin/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..f5e196e0bd9d0b4708d8a0405b20766c5b836b86 --- /dev/null +++ b/arkui_noninterop_plugin/tsconfig.json @@ -0,0 +1,32 @@ +{ + "extends": "@tsconfig/recommended/tsconfig.json", + "compilerOptions": { + "target": "es2021", + "module": "commonjs", + "moduleResolution": "node", + "composite": true, + "incremental": true, + "declarationMap": true, + "sourceMap": true, + "declaration": true, + "noEmitOnError": true, + "strict": true, + "skipLibCheck": false, + "removeComments": false, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "baseUrl": ".", + "rootDir": "./src", + "types": [ + "node", + ], + "lib": [ + "es2021", + ] + }, + "include": [ + "./src/add_import/*.ts", + "./src/collect_symbol/*.ts", + "./src/delete_noninterop/*.ts", + ] +}