diff --git a/ets2panda/linter/src/lib/CookBookMsg.ts b/ets2panda/linter/src/lib/CookBookMsg.ts index 3a9d31e00111acd399db23ef34f17b9c0ff44281..f194a37a1d83a7f7775e0edc369b4eb8666cc604 100644 --- a/ets2panda/linter/src/lib/CookBookMsg.ts +++ b/ets2panda/linter/src/lib/CookBookMsg.ts @@ -352,6 +352,8 @@ cookBookTag[350] = 'The taskpool setCloneList interface is deleted from ArkTS1.2 (arkts-limited-stdlib-no-setCloneList)'; cookBookTag[351] = 'The taskpool setTransferList interface is deleted from ArkTS1.2 (arkts-limited-stdlib-no-setTransferList)'; +cookBookTag[352] = + '1.2 Void cannot be combined. OnDestroy/onDisconnect (The return type of the method is now void | Promise) needs to be split into two interfaces. (sdk-ability-asynchronous-lifecycle)'; cookBookTag[355] = 'Usage of standard library is restricted(arkts-limited-stdlib-no-sendable-decorator)'; cookBookTag[356] = 'Usage of standard library is restricted(arkts-limited-stdlib-no-concurrent-decorator)'; cookBookTag[357] = 'Worker are not supported(arkts-no-need-stdlib-worker)'; diff --git a/ets2panda/linter/src/lib/FaultAttrs.ts b/ets2panda/linter/src/lib/FaultAttrs.ts index d06f179b1c97cd3b8e9cd6956fc00416fb9ee1d5..23bf229546b8be88ccc376448cb4baf3f4cfe369 100644 --- a/ets2panda/linter/src/lib/FaultAttrs.ts +++ b/ets2panda/linter/src/lib/FaultAttrs.ts @@ -250,6 +250,7 @@ faultsAttrs[FaultID.BuiltinNoCtorFunc] = new FaultAttributes(348); faultsAttrs[FaultID.SharedArrayBufferDeprecated] = new FaultAttributes(349); faultsAttrs[FaultID.SetCloneListDeprecated] = new FaultAttributes(350); faultsAttrs[FaultID.SetTransferListDeprecated] = new FaultAttributes(351); +faultsAttrs[FaultID.SdkAbilityAsynchronousLifecycle] = new FaultAttributes(352); faultsAttrs[FaultID.LimitedStdLibNoSendableDecorator] = new FaultAttributes(355); faultsAttrs[FaultID.LimitedStdLibNoDoncurrentDecorator] = new FaultAttributes(356); faultsAttrs[FaultID.NoNeedStdlibWorker] = new FaultAttributes(357); diff --git a/ets2panda/linter/src/lib/FaultDesc.ts b/ets2panda/linter/src/lib/FaultDesc.ts index 5e59068df3d83e7bc5d38e07edfe934761584bfb..48756290d5e32be5d8d4eb3bcf4c00922aac03e4 100644 --- a/ets2panda/linter/src/lib/FaultDesc.ts +++ b/ets2panda/linter/src/lib/FaultDesc.ts @@ -229,6 +229,7 @@ faultDesc[FaultID.BuiltinNoCtorFunc] = 'Api is not support ctor-signature and ca faultDesc[FaultID.SharedArrayBufferDeprecated] = 'SharedArrayBuffer is not supported'; faultDesc[FaultID.SetCloneListDeprecated] = 'setCloneList is not supported'; faultDesc[FaultID.SetTransferListDeprecated] = 'setTransferList is not supported'; +faultDesc[FaultID.SdkAbilityAsynchronousLifecycle] = '1.2 Void cannot be combined'; faultDesc[FaultID.LimitedStdLibNoSendableDecorator] = 'Limited stdlib no sendable decorator'; faultDesc[FaultID.LimitedStdLibNoDoncurrentDecorator] = 'Limited stdlib no concurrent decorator'; faultDesc[FaultID.NoNeedStdlibWorker] = 'No need stdlib worker'; diff --git a/ets2panda/linter/src/lib/Problems.ts b/ets2panda/linter/src/lib/Problems.ts index fae33a9b99505982c2d90ae76d0903feb8ebffed..383d0eb25eb67f815633fb474d1ecb8b34225aa8 100644 --- a/ets2panda/linter/src/lib/Problems.ts +++ b/ets2panda/linter/src/lib/Problems.ts @@ -230,6 +230,7 @@ export enum FaultID { SharedArrayBufferDeprecated, SetCloneListDeprecated, SetTransferListDeprecated, + SdkAbilityAsynchronousLifecycle, LimitedStdLibNoSendableDecorator, LimitedStdLibNoDoncurrentDecorator, NoNeedStdlibWorker, diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index 1ba26f68a66fd4705c83ef78e502ecdc7a6cb8b8..400a0130d9d9da334d717f9c0500035edb8ba0a3 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -144,6 +144,15 @@ import type { ArrayAccess, UncheckedIdentifier } from './utils/consts/RuntimeChe import { NUMBER_LITERAL } from './utils/consts/RuntimeCheckAPI'; import { globalApiAssociatedInfo } from './utils/consts/AssociatedInfo'; import { ARRAY_API_LIST } from './utils/consts/ArraysAPI'; +import { + ABILITY_KIT, + ASYNC_LIFECYCLE_SDK_LIST, + ON_DESTROY, + ON_DISCONNECT, + PROMISE, + SERVICE_EXTENSION_ABILITY, + VOID +} from './utils/consts/AsyncLifecycleSDK'; import { ERROR_PROP_LIST } from './utils/consts/ErrorProp'; import { D_ETS, D_TS } from './utils/consts/TsSuffix'; @@ -3362,6 +3371,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { this.handleMethodInherit(tsMethodDecl); this.handleSdkGlobalApi(tsMethodDecl); this.handleLimitedVoidFunction(tsMethodDecl); + this.checkVoidLifecycleReturn(tsMethodDecl); } private handleLimitedVoidFunction(node: ts.FunctionLikeDeclaration): void { @@ -8173,6 +8183,152 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { } } + /** + * Returns true if the method’s declared return type or body returns Promise. + */ + private hasPromiseVoidReturn(method: ts.MethodDeclaration): boolean { + return ( + this.hasAnnotatedPromiseVoidReturn(method) || this.isAsyncMethod(method) || this.hasBodyPromiseReturn(method) + ); + } + + /** + * Checks if the method’s declared return type annotation includes Promise. + */ + private hasAnnotatedPromiseVoidReturn(method: ts.MethodDeclaration): boolean { + void this; + if (!method.type) { + return false; + } + const t = method.type; + // Union type check + if (ts.isUnionTypeNode(t)) { + return t.types.some((u) => { + return this.isSinglePromiseVoid(u); + }); + } + // Single Promise check + return this.isSinglePromiseVoid(t); + } + + private isSinglePromiseVoid(n: ts.Node): boolean { + void this; + return ts.isTypeReferenceNode(n) && n.typeName.getText() === PROMISE && n.typeArguments?.[0]?.getText() === VOID; + } + + /** + * Checks if the method is declared async (implying Promise return). + */ + private isAsyncMethod(method: ts.MethodDeclaration): boolean { + void this; + return ( + method.modifiers?.some((m) => { + return m.kind === ts.SyntaxKind.AsyncKeyword; + }) ?? false + ); + } + + /** + * Scans the method body iteratively for any Promise-returning statements. + */ + private hasBodyPromiseReturn(method: ts.MethodDeclaration): boolean { + if (!method.body) { + return false; + } + + let found = false; + const visit = (node: ts.Node): void => { + if (ts.isReturnStatement(node) && node.expression) { + const retType = this.tsTypeChecker.getTypeAtLocation(node.expression); + if (retType.symbol?.getName() === PROMISE) { + found = true; + return; + } + } + ts.forEachChild(node, visit); + }; + ts.forEachChild(method.body, visit); + + return found; + } + + /** + * Returns true if this method name is onDestroy/onDisconnect and class extends one of the supported Ability subclasses. + */ + private isLifecycleMethodOnAbilitySubclass(method: ts.MethodDeclaration): boolean { + const name = method.name.getText(); + if (name !== ON_DESTROY && name !== ON_DISCONNECT) { + return false; + } + const cls = method.parent; + if (!ts.isClassDeclaration(cls) || !cls.heritageClauses) { + return false; + } + return cls.heritageClauses.some((h) => { + return ( + h.token === ts.SyntaxKind.ExtendsKeyword && + h.types.some((tn) => { + return this.isSupportedAbilityBase(method.name.getText(), tn.expression); + }) + ); + }); + } + + /** + * Checks that the base class name and its import source or declaration file are supported, + * and matches the lifecycle method (onDestroy vs onDisconnect). + */ + private isSupportedAbilityBase(methodName: string, baseExprNode: ts.Expression): boolean { + const sym = this.tsTypeChecker.getSymbolAtLocation(baseExprNode); + if (!sym) { + return false; + } + + const baseName = sym.getName(); + if (!ASYNC_LIFECYCLE_SDK_LIST.has(baseName)) { + return false; + } + + if (methodName === ON_DISCONNECT && baseName !== SERVICE_EXTENSION_ABILITY) { + return false; + } + if (methodName === ON_DESTROY && baseName === SERVICE_EXTENSION_ABILITY) { + return false; + } + + const decl = sym.getDeclarations()?.[0]; + if (!decl || !ts.isImportSpecifier(decl)) { + return false; + } + + const importDecl = decl.parent.parent.parent; + const moduleName = (importDecl.moduleSpecifier as ts.StringLiteral).text; + const srcFile = decl.getSourceFile().fileName; + + return moduleName === ABILITY_KIT || srcFile.endsWith(`${baseName}.${EXTNAME_D_TS}`); + } + + /** + * Rule sdk-void-lifecycle-return: + * Flags onDestroy/onDisconnect methods in Ability subclasses + * whose return type includes Promise. + */ + private checkVoidLifecycleReturn(method: ts.MethodDeclaration): void { + if (!this.options.arkts2) { + return; + } + + if (!this.isLifecycleMethodOnAbilitySubclass(method)) { + return; + } + + if (!this.hasPromiseVoidReturn(method)) { + return; + } + + this.incrementCounters(method.name, FaultID.SdkAbilityAsynchronousLifecycle); + } + private handleGetOwnPropertyNames(decl: ts.PropertyAccessExpression): void { if (this.checkPropertyAccessExpression(decl, GET_OWN_PROPERTY_NAMES_TEXT, TypeScriptLinter.missingAttributeSet)) { const autofix = this.autofixer?.fixMissingAttribute(decl); diff --git a/ets2panda/linter/src/lib/utils/consts/ArkTS2Rules.ts b/ets2panda/linter/src/lib/utils/consts/ArkTS2Rules.ts index 1f26e36e862aa393385cf635d5b726a41ec9c02c..ed63efd46e1288ea9509afcc30896af0c05ddf80 100644 --- a/ets2panda/linter/src/lib/utils/consts/ArkTS2Rules.ts +++ b/ets2panda/linter/src/lib/utils/consts/ArkTS2Rules.ts @@ -136,6 +136,7 @@ export const arkts2Rules: number[] = [ 349, 350, 351, + 352, 355, 356, 357, diff --git a/ets2panda/linter/src/lib/utils/consts/AsyncLifecycleSDK.ts b/ets2panda/linter/src/lib/utils/consts/AsyncLifecycleSDK.ts new file mode 100644 index 0000000000000000000000000000000000000000..b589d85b4fee72146258cba77e3403245fa0355e --- /dev/null +++ b/ets2panda/linter/src/lib/utils/consts/AsyncLifecycleSDK.ts @@ -0,0 +1,31 @@ +/* + * 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 VOID = 'Void'; +export const PROMISE = 'Promise'; + +export const ON_DESTROY = 'onDestroy'; +export const ON_DISCONNECT = 'onDisconnect'; + +export const SERVICE_EXTENSION_ABILITY = 'ServiceExtensionAbility'; + +export const ABILITY_KIT = '@kit.AbilityKit'; + +export const ASYNC_LIFECYCLE_SDK_LIST = new Set([ + 'UIAbility', + 'UIExtensionAbility', + 'AutoFillExtensionAbility', + 'ServiceExtensionAbility' +]); diff --git a/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets b/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets new file mode 100644 index 0000000000000000000000000000000000000000..71d8cd2396a17b3ef5261f5ddb4d43efe5230b73 --- /dev/null +++ b/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets @@ -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 { UIAbility } from '@kit.AbilityKit'; + +function sleep(ms: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms) + }) +} +export default class MyUIAbility extends UIAbility { + async onDestroy(): Promise { // ❌ Error + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + return sleep(1000); + } +} + + +function sleep(ms: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms) + }) +} +export default class MyUIAbility extends UIAbility { + onDestroy() { // ❌ Error + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + return sleep(1000); + } +} diff --git a/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.args.json b/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.args.json new file mode 100644 index 0000000000000000000000000000000000000000..66fb88f85945924e8be0e83d90123507033f4c5d --- /dev/null +++ b/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.args.json @@ -0,0 +1,19 @@ +{ + "copyright": [ + "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." + ], + "mode": { + "arkts2": "" + } +} diff --git a/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.arkts2.json b/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.arkts2.json new file mode 100644 index 0000000000000000000000000000000000000000..6d099918006dbaec831877c173cbbeee70235071 --- /dev/null +++ b/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.arkts2.json @@ -0,0 +1,148 @@ +{ + "copyright": [ + "Copyright (c) 2024 Huawei Device Co., Ltd.", + "Licensed under the Apache License, Version 2.0 (the 'License');", + "you may not use this file except in compliance with the License.", + "You may obtain a copy of the License at", + "", + "http://www.apache.org/licenses/LICENSE-2.0", + "", + "Unless required by applicable law or agreed to in writing, software", + "distributed under the License is distributed on an 'AS IS' BASIS,", + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.", + "See the License for the specific language governing permissions and", + "limitations under the License." + ], + "result": [ + { + "line": 18, + "column": 1, + "endLine": 22, + "endColumn": 2, + "problem": "TsOverload", + "suggest": "", + "rule": "Class TS overloading is not supported(arkts-no-ts-overload)", + "severity": "ERROR" + }, + { + "line": 19, + "column": 14, + "endLine": 19, + "endColumn": 21, + "problem": "ConstructorIfaceFromSdk", + "suggest": "", + "rule": "Construct signatures are not supported in interfaces.(sdk-ctor-signatures-iface)", + "severity": "ERROR" + }, + { + "line": 19, + "column": 10, + "endLine": 21, + "endColumn": 5, + "problem": "GenericCallNoTypeArgs", + "suggest": "", + "rule": "Type inference in case of generic function calls is limited (arkts-no-inferred-generic-params)", + "severity": "ERROR" + }, + { + "line": 24, + "column": 3, + "endLine": 27, + "endColumn": 4, + "problem": "LimitedVoidTypeFromSdk", + "suggest": "", + "rule": "Type \"void\" has no instances.(sdk-limited-void-type)", + "severity": "ERROR" + }, + { + "line": 24, + "column": 9, + "endLine": 24, + "endColumn": 18, + "problem": "SdkAbilityAsynchronousLifecycle", + "suggest": "", + "rule": "1.2 Void cannot be combined. OnDestroy/onDisconnect (The return type of the method is now void | Promise) needs to be split into two interfaces. (sdk-ability-asynchronous-lifecycle)", + "severity": "ERROR" + }, + { + "line": 26, + "column": 12, + "endLine": 26, + "endColumn": 23, + "problem": "StructuralIdentity", + "suggest": "", + "rule": "Structural typing is not supported (arkts-no-structural-typing)", + "severity": "ERROR" + }, + { + "line": 26, + "column": 18, + "endLine": 26, + "endColumn": 22, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 31, + "column": 1, + "endLine": 35, + "endColumn": 2, + "problem": "TsOverload", + "suggest": "", + "rule": "Class TS overloading is not supported(arkts-no-ts-overload)", + "severity": "ERROR" + }, + { + "line": 32, + "column": 14, + "endLine": 32, + "endColumn": 21, + "problem": "ConstructorIfaceFromSdk", + "suggest": "", + "rule": "Construct signatures are not supported in interfaces.(sdk-ctor-signatures-iface)", + "severity": "ERROR" + }, + { + "line": 32, + "column": 10, + "endLine": 34, + "endColumn": 5, + "problem": "GenericCallNoTypeArgs", + "suggest": "", + "rule": "Type inference in case of generic function calls is limited (arkts-no-inferred-generic-params)", + "severity": "ERROR" + }, + { + "line": 37, + "column": 3, + "endLine": 40, + "endColumn": 4, + "problem": "LimitedVoidTypeFromSdk", + "suggest": "", + "rule": "Type \"void\" has no instances.(sdk-limited-void-type)", + "severity": "ERROR" + }, + { + "line": 37, + "column": 3, + "endLine": 37, + "endColumn": 12, + "problem": "SdkAbilityAsynchronousLifecycle", + "suggest": "", + "rule": "1.2 Void cannot be combined. OnDestroy/onDisconnect (The return type of the method is now void | Promise) needs to be split into two interfaces. (sdk-ability-asynchronous-lifecycle)", + "severity": "ERROR" + }, + { + "line": 39, + "column": 18, + "endLine": 39, + "endColumn": 22, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + } + ] +} diff --git a/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.json b/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.json new file mode 100644 index 0000000000000000000000000000000000000000..dd03fcf5442488620bcd4b3447f0fcdd89e1905b --- /dev/null +++ b/ets2panda/linter/test/sdk_ability_asynchronous_lifecycle.ets.json @@ -0,0 +1,17 @@ +{ + "copyright": [ + "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." + ], + "result": [] +}