diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index 2658cb6a0d25c67bc325d128ec695ae4c4794efe..8ac0bd1fb1d7e1a5c5da3cd872c81bf9950883f8 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -4272,7 +4272,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { if (!this.isTypeSameOrWider(baseParamType, derivedParamType)) { this.incrementCounters(derivedParams[i], FaultID.MethodInheritRule); - } + } } } @@ -4402,7 +4402,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { } if (this.checkTypeInheritance(derivedType, baseType, false)) { - return true; + return true; } const baseTypeSet = new Set(this.flattenUnionTypes(baseType)); @@ -4438,7 +4438,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { } } - if(this.checkTypeInheritance(fromType, toType)) { + if (this.checkTypeInheritance(fromType, toType)) { return true; } @@ -4453,11 +4453,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { }); } - private checkTypeInheritance( - sourceType: ts.Type, - targetType: ts.Type, - isSouceTotaqrget: boolean = true - ): boolean { + private checkTypeInheritance(sourceType: ts.Type, targetType: ts.Type, isSouceTotaqrget: boolean = true): boolean { // Early return if either type lacks symbol information if (!sourceType.symbol || !targetType.symbol) { return false; @@ -4469,7 +4465,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { // Get inheritance chain and check for relationship const inheritanceChain = this.getTypeInheritanceChain(typeToGetChain); - return inheritanceChain.some(t => { + return inheritanceChain.some((t) => { return t.symbol === typeToCheck.symbol; }); } @@ -4479,12 +4475,14 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { const declarations = type.symbol?.getDeclarations() || []; for (const declaration of declarations) { - if ((!ts.isClassDeclaration(declaration) && !ts.isInterfaceDeclaration(declaration)) || - !declaration.heritageClauses) { + if ( + !ts.isClassDeclaration(declaration) && !ts.isInterfaceDeclaration(declaration) || + !declaration.heritageClauses + ) { continue; } - const heritageClauses = declaration.heritageClauses.filter(clause => { + const heritageClauses = declaration.heritageClauses.filter((clause) => { return clause.token === ts.SyntaxKind.ExtendsKeyword || clause.token === ts.SyntaxKind.ImplementsKeyword; }); @@ -5447,52 +5445,146 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { } } - private handleCallExpression(node: ts.Node): void { - const tsCallExpr = node as ts.CallExpression; - this.checkSdkAbilityLifecycleMonitor(tsCallExpr); - this.handleCallExpressionForUI(tsCallExpr); - this.handleBuiltinCtorCallSignature(tsCallExpr); - this.handleSdkConstructorIfaceForCallExpression(tsCallExpr); - if (this.options.arkts2 && tsCallExpr.typeArguments !== undefined) { - this.handleSdkPropertyAccessByIndex(tsCallExpr); + private handleCallExpression(callExpr: ts.CallExpression): void { + this.checkSdkAbilityLifecycleMonitor(callExpr); + this.handleCallExpressionForUI(callExpr); + this.handleBuiltinCtorCallSignature(callExpr); + this.handleSdkConstructorIfaceForCallExpression(callExpr); + if (this.options.arkts2 && callExpr.typeArguments !== undefined) { + this.handleSdkPropertyAccessByIndex(callExpr); } - const calleeSym = this.tsUtils.trueSymbolAtLocation(tsCallExpr.expression); - const callSignature = this.tsTypeChecker.getResolvedSignature(tsCallExpr); - this.handleImportCall(tsCallExpr); - this.handleRequireCall(tsCallExpr); + const calleeSym = this.tsUtils.trueSymbolAtLocation(callExpr.expression); + const callSignature = this.tsTypeChecker.getResolvedSignature(callExpr); + this.handleImportCall(callExpr); + this.handleRequireCall(callExpr); if (calleeSym !== undefined) { - this.processCalleeSym(calleeSym, tsCallExpr); + this.processCalleeSym(calleeSym, callExpr); } if (callSignature !== undefined) { if (!this.tsUtils.isLibrarySymbol(calleeSym)) { - this.handleStructIdentAndUndefinedInArgs(tsCallExpr, callSignature); - this.handleGenericCallWithNoTypeArgs(tsCallExpr, callSignature); + this.handleStructIdentAndUndefinedInArgs(callExpr, callSignature); + this.handleGenericCallWithNoTypeArgs(callExpr, callSignature); } else if (this.options.arkts2) { - this.handleGenericCallWithNoTypeArgs(tsCallExpr, callSignature); + this.handleGenericCallWithNoTypeArgs(callExpr, callSignature); } - this.handleNotsLikeSmartTypeOnCallExpression(tsCallExpr, callSignature); + this.handleNotsLikeSmartTypeOnCallExpression(callExpr, callSignature); } - this.handleInteropForCallExpression(tsCallExpr); - this.handleLibraryTypeCall(tsCallExpr); + this.handleInteropForCallExpression(callExpr); + this.handleLibraryTypeCall(callExpr); if ( - ts.isPropertyAccessExpression(tsCallExpr.expression) && - this.tsUtils.hasEsObjectType(tsCallExpr.expression.expression) + ts.isPropertyAccessExpression(callExpr.expression) && + this.tsUtils.hasEsObjectType(callExpr.expression.expression) ) { const faultId = this.options.arkts2 ? FaultID.EsValueTypeError : FaultID.EsValueType; - this.incrementCounters(node, faultId); + this.incrementCounters(callExpr, faultId); + } + this.handleLimitedVoidWithCall(callExpr); + this.fixJsImportCallExpression(callExpr); + this.handleInteropForCallJSExpression(callExpr, calleeSym, callSignature); + this.handleNoTsLikeFunctionCall(callExpr); + this.handleObjectLiteralInFunctionArgs(callExpr); + this.handleSdkGlobalApi(callExpr); + this.handleObjectLiteralAssignmentToClass(callExpr); + this.checkRestrictedAPICall(callExpr); + this.handleNoDeprecatedApi(callExpr); + this.handleFunctionReturnThisCall(callExpr); + this.handlePromiseTupleGeneric(callExpr); + this.checkArgumentTypeOfCallExpr(callExpr, callSignature); + this.handleTupleGeneric(callExpr); + } + + private checkArgumentTypeOfCallExpr(callExpr: ts.CallExpression, signature: ts.Signature | undefined): void { + if (!this.options.arkts2) { + return; + } + if (!signature) { + return; + } + + const args = callExpr.arguments; + if (args.length === 0) { + return; + } + + for (const [idx, arg] of args.entries()) { + this.isArgumentAndParameterMatch(signature, arg, idx); + } + } + + private isArgumentAndParameterMatch(signature: ts.Signature, arg: ts.Expression, idx: number): void { + if (!ts.isPropertyAccessExpression(arg)) { + return; + } + + let rootObject = arg.expression; + + while (ts.isPropertyAccessExpression(rootObject)) { + rootObject = rootObject.expression; + } + + if (rootObject.kind !== ts.SyntaxKind.ThisKeyword) { + return; + } + + const param = signature.parameters.at(idx); + if (!param) { + return; + } + const paramDecl = param.getDeclarations(); + if (!paramDecl || paramDecl.length === 0) { + return; + } + + const paramFirstDecl = paramDecl[0]; + if (!ts.isParameter(paramFirstDecl)) { + return; + } + + const paramTypeNode = paramFirstDecl.type; + const argumentType = this.tsTypeChecker.getTypeAtLocation(arg); + if (!paramTypeNode) { + return; + } + + if (!paramTypeNode) { + return; + } + + const argumentTypeString = this.tsTypeChecker.typeToString(argumentType); + if (ts.isUnionTypeNode(paramTypeNode)) { + this.checkUnionTypesMatching(arg, paramTypeNode, argumentTypeString); + } else { + this.checkSingleTypeMatching(paramTypeNode, arg, argumentTypeString); + } + } + + private checkSingleTypeMatching(paramTypeNode: ts.TypeNode, arg: ts.Node, argumentTypeString: string): void { + const paramType = this.tsTypeChecker.getTypeFromTypeNode(paramTypeNode); + const paramTypeString = this.tsTypeChecker.typeToString(paramType); + if (TsUtils.isIgnoredTypeForParameterType(paramTypeString, paramType)) { + return; + } + + if (argumentTypeString !== paramTypeString) { + this.incrementCounters(arg, FaultID.StructuralIdentity); + } + } + + private checkUnionTypesMatching(arg: ts.Node, paramTypeNode: ts.UnionTypeNode, argumentTypeString: string): void { + let notMatching = true; + for (const type of paramTypeNode.types) { + const paramType = this.tsTypeChecker.getTypeFromTypeNode(type); + const paramTypeString = this.tsTypeChecker.typeToString(paramType); + notMatching = !TsUtils.isIgnoredTypeForParameterType(paramTypeString, paramType); + + if (argumentTypeString === paramTypeString) { + notMatching = false; + } + } + + if (notMatching) { + this.incrementCounters(arg, FaultID.StructuralIdentity); } - this.handleLimitedVoidWithCall(tsCallExpr); - this.fixJsImportCallExpression(tsCallExpr); - this.handleInteropForCallJSExpression(tsCallExpr, calleeSym, callSignature); - this.handleNoTsLikeFunctionCall(tsCallExpr); - this.handleObjectLiteralInFunctionArgs(tsCallExpr); - this.handleSdkGlobalApi(tsCallExpr); - this.handleObjectLiteralAssignmentToClass(tsCallExpr); - this.checkRestrictedAPICall(tsCallExpr); - this.handleNoDeprecatedApi(tsCallExpr); - this.handleFunctionReturnThisCall(tsCallExpr); - this.handlePromiseTupleGeneric(tsCallExpr); - this.handleTupleGeneric(tsCallExpr); } private handleTupleGeneric(callExpr: ts.CallExpression): void { diff --git a/ets2panda/linter/src/lib/utils/TsUtils.ts b/ets2panda/linter/src/lib/utils/TsUtils.ts index fbc408c87e9af7c1444b9687e3de92829d240512..1eea1826ef462274b8813ba8b2dd1429d51b203b 100644 --- a/ets2panda/linter/src/lib/utils/TsUtils.ts +++ b/ets2panda/linter/src/lib/utils/TsUtils.ts @@ -48,6 +48,7 @@ import { ETS_MODULE, PATH_SEPARATOR, VALID_OHM_COMPONENTS_MODULE_PATH } from './ import { EXTNAME_ETS, EXTNAME_JS, EXTNAME_D_ETS } from './consts/ExtensionName'; import { CONCAT_ARRAY, STRING_ERROR_LITERAL } from './consts/Literals'; import { INT_MIN, INT_MAX } from './consts/NumericalConstants'; +import { IGNORE_TYPE_LIST } from './consts/TypesToBeIgnored'; export const PROMISE_METHODS = new Set(['all', 'race', 'any', 'resolve', 'allSettled']); export const PROMISE_METHODS_WITH_NO_TUPLE_SUPPORT = new Set(['all', 'race', 'any', 'allSettled']); @@ -3917,4 +3918,16 @@ export class TsUtils { return (typeArguments[0].flags & ts.TypeFlags.Number) !== 0; } + + static isIgnoredTypeForParameterType(typeString: string, type: ts.Type): boolean { + if (TsUtils.isAnyType(type)) { + return true; + } + + return ( + IGNORE_TYPE_LIST.findIndex((ignored_type) => { + return typeString.includes(ignored_type); + }) !== -1 + ); + } } diff --git a/ets2panda/linter/src/lib/utils/consts/TypesToBeIgnored.ts b/ets2panda/linter/src/lib/utils/consts/TypesToBeIgnored.ts new file mode 100644 index 0000000000000000000000000000000000000000..06b56ad53c8ca0351745034ce98cf18c73b1e32d --- /dev/null +++ b/ets2panda/linter/src/lib/utils/consts/TypesToBeIgnored.ts @@ -0,0 +1,16 @@ +/* + * 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 IGNORE_TYPE_LIST: string[] = ['any[]', 'Promise', 'never', 'never[]', '{}', 'undefined[]']; diff --git a/ets2panda/linter/test/main/call_expression_matching_argument.ets b/ets2panda/linter/test/main/call_expression_matching_argument.ets new file mode 100644 index 0000000000000000000000000000000000000000..08c34d7682b9a6c397ff28078f1fc35f2f4c584b --- /dev/null +++ b/ets2panda/linter/test/main/call_expression_matching_argument.ets @@ -0,0 +1,70 @@ +/* + * 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. + */ + +interface A { + value: string; +} + +interface Outer { + inner: A +} + +interface No { + inner: string +} + +class Test { + sv1: number = 1 + sv2: string = "some Value"; + sv3: boolean = true; + sv4: A = { value: "something" } + sv5: Outer = { inner: sv4 } + sv6: No = { inner: sv2 } + sv7: Array = [sv2, sv2, sv2]; + + someMethod() { + func(this.sv2) //error + func(this.sv4) //valid + func(this.sv6.inner) // error + func(this.sv5.inner) //valid + func2(this.sv2) //valid + func2(this.sv4) //valid + func3(this.sv2) //valid + func4(this.sv2, this.sv1) //valid + func5(this.sv7) //valid + } +} + + +function func(param: A) { + console.log(A); +} + +function func2(param: A | any) { + console.log(param); +} + +function func3(param: string | Promise) { + console.log(param); +} + +function func4(param: string, param2: any[]) { + console.log(param, ...param2); +} + +function func5(param: Array) { + param.forEach(elem => console.log(elem)) +} + diff --git a/ets2panda/linter/test/main/call_expression_matching_argument.ets.args.json b/ets2panda/linter/test/main/call_expression_matching_argument.ets.args.json new file mode 100644 index 0000000000000000000000000000000000000000..bc4d2071daf6e9354e711c3b74b6be2b56659066 --- /dev/null +++ b/ets2panda/linter/test/main/call_expression_matching_argument.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/main/call_expression_matching_argument.ets.arkts2.json b/ets2panda/linter/test/main/call_expression_matching_argument.ets.arkts2.json new file mode 100644 index 0000000000000000000000000000000000000000..00147aa39226a33dc4ae27c875ec669c70be8045 --- /dev/null +++ b/ets2panda/linter/test/main/call_expression_matching_argument.ets.arkts2.json @@ -0,0 +1,58 @@ +{ + "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": [ + { + "line": 38, + "column": 14, + "endLine": 38, + "endColumn": 22, + "problem": "StructuralIdentity", + "suggest": "", + "rule": "Structural typing is not supported (arkts-no-structural-typing)", + "severity": "ERROR" + }, + { + "line": 40, + "column": 14, + "endLine": 40, + "endColumn": 28, + "problem": "StructuralIdentity", + "suggest": "", + "rule": "Structural typing is not supported (arkts-no-structural-typing)", + "severity": "ERROR" + }, + { + "line": 55, + "column": 27, + "endLine": 55, + "endColumn": 30, + "problem": "AnyType", + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)", + "severity": "ERROR" + }, + { + "line": 63, + "column": 39, + "endLine": 63, + "endColumn": 42, + "problem": "AnyType", + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)", + "severity": "ERROR" + } + ] +} \ No newline at end of file diff --git a/ets2panda/linter/test/main/call_expression_matching_argument.ets.json b/ets2panda/linter/test/main/call_expression_matching_argument.ets.json new file mode 100644 index 0000000000000000000000000000000000000000..f864fd1cc3f376222369e712dbcee0ed60b91641 --- /dev/null +++ b/ets2panda/linter/test/main/call_expression_matching_argument.ets.json @@ -0,0 +1,38 @@ +{ + "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": [ + { + "line": 55, + "column": 27, + "endLine": 55, + "endColumn": 30, + "problem": "AnyType", + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)", + "severity": "ERROR" + }, + { + "line": 63, + "column": 39, + "endLine": 63, + "endColumn": 42, + "problem": "AnyType", + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)", + "severity": "ERROR" + } + ] +} \ No newline at end of file