diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index cb56e385e10b892c8b8e5a8e9f61b1c3f0c7608e..317b9035bda51f21a4a07eddc2ffe217a6395344 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -226,6 +226,7 @@ import { ES_OBJECT } from './utils/consts/ESObject'; import { cookBookMsg } from './CookBookMsg'; import { getCommonApiInfoMap } from './utils/functions/CommonApiInfo'; import { arkuiDecoratorSet } from './utils/consts/ArkuiDecorator'; +import { ABILITY_LIFECYCLE, ABILITY_LIFECYCLE_CALLBACK } from './utils/consts/LifecycleMonitor'; export class TypeScriptLinter extends BaseTypeScriptLinter { supportedStdCallApiChecker: SupportedStdCallApiChecker; @@ -10404,6 +10405,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { if (!ts.isClassDeclaration(cls) || !cls.heritageClauses) { return false; } + return cls.heritageClauses.some((h) => { return ( h.token === ts.SyntaxKind.ExtendsKeyword && @@ -10419,16 +10421,12 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { * and matches the lifecycle method (onDestroy vs onDisconnect). */ private isSupportedAbilityBase(methodName: string, baseExprNode: ts.Expression): boolean { - const sym = this.tsTypeChecker.getSymbolAtLocation(baseExprNode); + const sym = this.getExtendedAsyncLifecycleClass(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; } @@ -10448,6 +10446,40 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { return moduleName === ABILITY_KIT || srcFile.endsWith(`${baseName}.${EXTNAME_D_TS}`); } + /* + * check if this extended class is among the classes we care about + * if not check if that extended class is extending a class in async lifecycle list + * if not again + */ + private getExtendedAsyncLifecycleClass(extendedClass: ts.Expression): undefined | ts.Symbol { + if (!ts.isIdentifier(extendedClass)) { + return undefined; + } + const sym = this.tsTypeChecker.getSymbolAtLocation(extendedClass); + if (!sym) { + return undefined; + } + + if (ASYNC_LIFECYCLE_SDK_LIST.has(sym.getName())) { + return sym; + } + + const decls = sym.getDeclarations(); + if (!decls) { + return undefined; + } + + const decl = decls[0]; + if (!ts.isClassDeclaration(decl) || !decl.heritageClauses) { + return undefined; + } + + const extendedExpression = decl.heritageClauses[0]; + const extendedType = extendedExpression.types[0]; + + return this.getExtendedAsyncLifecycleClass(extendedType.expression); + } + /** * Rule sdk-void-lifecycle-return: * Flags onDestroy/onDisconnect methods in Ability subclasses @@ -10735,35 +10767,49 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { return; } - // Guard: exactly two arguments + const decl = this.getDeclarationOfLifecycleEventCallback(callExpr); + + if ( + !decl?.type || + !ts.isTypeReferenceNode(decl.type) || + decl.type.typeName.getText() !== ABILITY_LIFECYCLE_CALLBACK + ) { + return; + } + + // Report the legacy callback usage + this.incrementCounters(callExpr, FaultID.SdkAbilityLifecycleMonitor); + } + + private getDeclarationOfLifecycleEventCallback(callExpr: ts.CallExpression): undefined | ts.VariableDeclaration { const args = callExpr.arguments; if (args.length !== 2) { - return; + return undefined; } // Guard: first arg must be string literal "abilityLifecycle" const eventArg = args[0]; - if (!ts.isStringLiteral(eventArg) || eventArg.text !== 'abilityLifecycle') { - return; + if (!ts.isStringLiteral(eventArg) || eventArg.text !== ABILITY_LIFECYCLE) { + return undefined; } // Guard: second arg must be a variable declared as AbilityLifecycleCallback - const cbArg = args[1]; - if (!ts.isIdentifier(cbArg)) { - return; + let cbArgIdent = args[1]; + while (!ts.isIdentifier(cbArgIdent)) { + if (!ts.isNewExpression(cbArgIdent)) { + return undefined; + } + cbArgIdent = cbArgIdent.expression; } - const varSym = this.tsUtils.trueSymbolAtLocation(cbArg); - const decl = varSym?.declarations?.find(ts.isVariableDeclaration); - if ( - !decl?.type || - !ts.isTypeReferenceNode(decl.type) || - decl.type.typeName.getText() !== 'AbilityLifecycleCallback' - ) { - return; + + if (cbArgIdent.text === ABILITY_LIFECYCLE_CALLBACK) { + this.incrementCounters(callExpr, FaultID.SdkAbilityLifecycleMonitor); + return undefined; } - // Report the legacy callback usage - this.incrementCounters(callExpr, FaultID.SdkAbilityLifecycleMonitor); + const varSym = this.tsUtils.trueSymbolAtLocation(cbArgIdent); + + return varSym?.declarations?.find(ts.isVariableDeclaration); } private isOnMethod(node: ts.CallExpression): boolean { diff --git a/ets2panda/linter/src/lib/autofixes/Autofixer.ts b/ets2panda/linter/src/lib/autofixes/Autofixer.ts index 1972b3e1e1474dcfca4edefa3c4e074bb2975c1a..c23c9127f9ec0fb73bb624265b5b0ccd45e9dc4e 100644 --- a/ets2panda/linter/src/lib/autofixes/Autofixer.ts +++ b/ets2panda/linter/src/lib/autofixes/Autofixer.ts @@ -5336,9 +5336,10 @@ export class Autofixer { const methodName = identifierReplacements.get(identifierText); if (methodName) { const accessExpr = Autofixer.createUIContextAccess(methodName); - const newExpression = identifierText === GET_CONTEXT - ? ts.factory.createCallChain(accessExpr, undefined, undefined, []) - : accessExpr; + const newExpression = + identifierText === GET_CONTEXT ? + ts.factory.createCallChain(accessExpr, undefined, undefined, []) : + accessExpr; const start = identifierText === GET_CONTEXT ? callExpr.getStart() : callExpr.expression.getStart(); const end = identifierText === GET_CONTEXT ? callExpr.getEnd() : callExpr.expression.getEnd(); const newText = this.printer.printNode(ts.EmitHint.Unspecified, newExpression, callExpr.getSourceFile()); @@ -5360,7 +5361,10 @@ export class Autofixer { ts.factory.createPropertyAccessExpression( ts.factory.createIdentifier(UI_CONTEXT), ts.factory.createIdentifier(GET_FOCUSED_UI_CONTEXT) - ), undefined, []), + ), + undefined, + [] + ), ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), ts.factory.createIdentifier(methodName) ); diff --git a/ets2panda/linter/src/lib/utils/consts/LifecycleMonitor.ts b/ets2panda/linter/src/lib/utils/consts/LifecycleMonitor.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5b85859382253e402d6562a02f23cf40302d043 --- /dev/null +++ b/ets2panda/linter/src/lib/utils/consts/LifecycleMonitor.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 ABILITY_LIFECYCLE_CALLBACK: string = 'AbilityLifecycleCallback'; +export const ABILITY_LIFECYCLE: string = 'abilityLifecycle'; diff --git a/ets2panda/linter/test/main/sdk_ability_asynchronous_lifecycle.ets b/ets2panda/linter/test/main/sdk_ability_asynchronous_lifecycle.ets index b3af1c4b7764f50f47272f85f4e3c83fe158323d..a92df18dfc311e1cc4cc38b301e6df4aaeca3fc3 100644 --- a/ets2panda/linter/test/main/sdk_ability_asynchronous_lifecycle.ets +++ b/ets2panda/linter/test/main/sdk_ability_asynchronous_lifecycle.ets @@ -68,4 +68,11 @@ export default class MyUIAbility6 extends UIAbility { hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); return sleep1(1000); } -} \ No newline at end of file +} + +export default class MyUIAbility7 extends MyUIAbility1 { + async onDestroy(): Promise { // use UIAbility onDestroy, should report error + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + return sleep(1000); + } +} diff --git a/ets2panda/linter/test/main/sdk_ability_asynchronous_lifecycle.ets.arkts2.json b/ets2panda/linter/test/main/sdk_ability_asynchronous_lifecycle.ets.arkts2.json index cfda66c38c923c0288d93859a6879749f93edc94..751ef530a453c2f6ef7933502336e7b3984e759f 100644 --- a/ets2panda/linter/test/main/sdk_ability_asynchronous_lifecycle.ets.arkts2.json +++ b/ets2panda/linter/test/main/sdk_ability_asynchronous_lifecycle.ets.arkts2.json @@ -123,6 +123,16 @@ "suggest": "", "rule": "Type \"void\" has no instances.(sdk-limited-void-type)", "severity": "ERROR" + }, + { + "line": 74, + "column": 9, + "endLine": 74, + "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" } ] } \ No newline at end of file diff --git a/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets b/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets index b3c46a743715c647bfe939c22f9f54d46d4dd7d6..3e32ebb360367196358b5f6bc837056fa5a2fc5d 100644 --- a/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets +++ b/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets @@ -48,6 +48,7 @@ class MyAbilityStage extends AbilityStage { } let applicationContext = this.context.getApplicationContext(); let lifecycleId = applicationContext.on('abilityLifecycle', abilityLifecycleCallback); // report error normally + let lifecycleId2 = applicationContext.on('abilityLifecycle', new AbilityLifecycleCallback()); // report error normally } } diff --git a/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets.arkts2.json b/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets.arkts2.json index 2eff60dbc93b5e7dab7c3462f48f250aade94026..2eb1086c7e926783132e2c5c5a25bfa78e4e2b72 100644 --- a/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets.arkts2.json +++ b/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets.arkts2.json @@ -55,9 +55,29 @@ "severity": "ERROR" }, { - "line": 57, + "line": 51, + "column": 24, + "endLine": 51, + "endColumn": 97, + "problem": "SdkAbilityLifecycleMonitor", + "suggest": "", + "rule": "The UIAbility of 1.2 needs to be listened by the new StaticAbilityLifecycleCallback. The original AbilityLifecycleCallback can only listen to the UIAbility of 1.1 (sdk-ability-lifecycle-monitor)", + "severity": "ERROR" + }, + { + "line": 51, + "column": 70, + "endLine": 51, + "endColumn": 94, + "problem": "DynamicCtorCall", + "suggest": "", + "rule": "\"new\" expression with dynamic constructor type is not supported (arkts-no-dynamic-ctor-call)", + "severity": "ERROR" + }, + { + "line": 58, "column": 7, - "endLine": 58, + "endLine": 59, "endColumn": 8, "problem": "ObjectLiteralProperty", "suggest": "", @@ -65,9 +85,9 @@ "severity": "ERROR" }, { - "line": 57, + "line": 58, "column": 23, - "endLine": 57, + "endLine": 58, "endColumn": 30, "problem": "ParameterType", "suggest": "", @@ -75,9 +95,9 @@ "severity": "ERROR" }, { - "line": 57, + "line": 58, "column": 23, - "endLine": 57, + "endLine": 58, "endColumn": 30, "problem": "AnyType", "suggest": "", @@ -85,9 +105,9 @@ "severity": "ERROR" }, { - "line": 85, + "line": 86, "column": 23, - "endLine": 85, + "endLine": 86, "endColumn": 90, "problem": "SdkAbilityLifecycleMonitor", "suggest": "", diff --git a/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets.json b/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets.json index 81beed97c616587a30286ec0e80e1fd643531b12..46f84c7255ec46516716e69c04252377e8ce4b9a 100644 --- a/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets.json +++ b/ets2panda/linter/test/main/sdk_ability_lifecycle_monitor.ets.json @@ -25,9 +25,9 @@ "severity": "ERROR" }, { - "line": 57, + "line": 58, "column": 23, - "endLine": 57, + "endLine": 58, "endColumn": 30, "problem": "AnyType", "suggest": "",