diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index ca04dbcfcd70cd084c01b8f28c23f983dd279534..6df04dd82c05367cbc7880796bf86d5e436a6baf 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -3302,35 +3302,71 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { const classType = this.tsTypeChecker.getTypeAtLocation(classDecl); const allBaseTypes = this.getAllBaseTypes(classType, classDecl); - if (!allBaseTypes || allBaseTypes.length === 0) { - return; - } const methodName = node.name.text; + if (allBaseTypes && allBaseTypes.length > 0) { + for (const baseType of allBaseTypes) { + const baseMethod = baseType.getProperty(methodName); + if (!baseMethod) { + continue; + } - for (const baseType of allBaseTypes) { - const baseMethod = baseType.getProperty(methodName); - if (!baseMethod) { - continue; - } + const baseMethodDecl = baseMethod.declarations?.find((d) => { + return ( + (ts.isMethodDeclaration(d) || ts.isMethodSignature(d)) && + this.tsTypeChecker.getTypeAtLocation(d.parent) === baseType + ); + }) as ts.MethodDeclaration | ts.MethodSignature; - const baseMethodDecl = baseMethod.declarations?.find((d) => { - return ( - (ts.isMethodDeclaration(d) || ts.isMethodSignature(d)) && - this.tsTypeChecker.getTypeAtLocation(d.parent) === baseType - ); - }) as ts.MethodDeclaration | ts.MethodSignature; + if (!baseMethodDecl) { + continue; + } + + this.checkMethodParameters(node, baseMethodDecl); - if (!baseMethodDecl) { + this.checkMethodReturnType(node, baseMethodDecl); + + break; + } + } + this.checkIncompatibleFunctionTypes(node); + } + + private checkIncompatibleFunctionTypes(method: ts.MethodDeclaration): void { + const declaredReturnType = this.getActualReturnType(method); + if (!declaredReturnType) { + return; + } + const returnStatements = this.collectReturnStatements(method); + const declaredReturnTypeStr = this.tsTypeChecker.typeToString(declaredReturnType); + for (const returnStmt of returnStatements) { + if (!returnStmt.expression) { continue; } + const actualReturnType = this.tsTypeChecker.getTypeAtLocation(returnStmt.expression); + const actualReturnTypeStr = this.tsTypeChecker.typeToString(actualReturnType); + if (declaredReturnTypeStr === actualReturnTypeStr) { + return; + } + if (this.isSubtypeByBaseTypesList(actualReturnType, declaredReturnType)) { + this.incrementCounters(returnStmt.expression, FaultID.IncompationbleFunctionType); + return; + } + } + } - this.checkMethodParameters(node, baseMethodDecl); + private collectReturnStatements(node: ts.Node): ts.ReturnStatement[] { + const result: ts.ReturnStatement[] = []; - this.checkMethodReturnType(node, baseMethodDecl); + ts.forEachChild(node, (child) => { + if (ts.isReturnStatement(child)) { + result.push(child); + } else { + result.push(...this.collectReturnStatements(child)); + } + }); - break; - } + return result; } private getAllBaseTypes(type: ts.Type, classDecl: ts.ClassDeclaration): ts.Type[] | undefined { @@ -3514,7 +3550,12 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { const baseTypeNode = baseDeclarations[0]; const derivedTypeNode = derivedDeclarations[0]; - if (ts.isClassDeclaration(baseTypeNode) && ts.isClassDeclaration(derivedTypeNode)) { + if ( + baseTypeNode && + derivedTypeNode && + ts.isClassDeclaration(baseTypeNode) && + ts.isClassDeclaration(derivedTypeNode) + ) { const baseTypes = this.tsTypeChecker.getTypeAtLocation(derivedTypeNode).getBaseTypes(); const baseTypesExtends = baseTypes?.some((t) => { return t === baseType; @@ -6739,15 +6780,12 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { if (!this.options.arkts2) { return; } - const isArray = - this.tsUtils.isOrDerivedFrom(lhsType, this.tsUtils.isArray) && - this.tsUtils.isOrDerivedFrom(rhsType, this.tsUtils.isArray); + const isArray = this.tsUtils.isArray(lhsType) && this.tsUtils.isArray(rhsType); const isTuple = this.tsUtils.isOrDerivedFrom(lhsType, TsUtils.isTuple) && this.tsUtils.isOrDerivedFrom(rhsType, TsUtils.isTuple); if (!((isArray || isTuple) && lhsType !== rhsType)) { return; } - const rhsTypeStr = this.tsTypeChecker.typeToString(rhsType); let lhsTypeStr = this.tsTypeChecker.typeToString(lhsType); if (rhsExpr && (this.isNullOrEmptyArray(rhsExpr) || ts.isArrayLiteralExpression(rhsExpr))) { @@ -6766,6 +6804,16 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { } } + private isSubtypeByBaseTypesList(baseType: ts.Type, actualType: ts.Type): boolean { + if (this.isTypeAssignable(actualType, baseType)) { + return true; + } + const actualBaseTypes = actualType.getBaseTypes() || []; + return actualBaseTypes.some((base) => { + return this.isSubtypeByBaseTypesList(baseType, base); + }); + } + private isNullOrEmptyArray(expr: ts.Expression): boolean { if (ts.isNewExpression(expr)) { const constructorSym = this.tsTypeChecker.getSymbolAtLocation(expr.expression); @@ -9944,11 +9992,34 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { private isExactlySameType(type1: ts.Type, type2: ts.Type): boolean { if (type2.getCallSignatures().length > 0) { const returnType = TsUtils.getFunctionReturnType(type2); - return returnType ? - this.tsTypeChecker.typeToString(type1) === this.tsTypeChecker.typeToString(returnType) : - false; + return returnType ? this.isExactlySameType(type1, returnType) : false; } - return this.tsTypeChecker.typeToString(type1) === this.tsTypeChecker.typeToString(type2); + + const type1String = this.tsTypeChecker.typeToString(type1); + const type2String = this.tsTypeChecker.typeToString(type2); + if (type1String === type2String) { + return true; + } + + if (this.checkBaseTypes(type1, type2) || this.checkBaseTypes(type2, type1)) { + return true; + } + return type1String === type2String; + } + + private checkBaseTypes(type1: ts.Type, type2: ts.Type): boolean { + const isClassType = + (type1.getFlags() & ts.TypeFlags.Object) !== 0 && + ((type1 as ts.ObjectType).objectFlags & ts.ObjectFlags.Class) !== 0; + if (isClassType) { + const baseTypes = (type1 as any).getBaseTypes?.() || []; + for (const baseType of baseTypes) { + if (this.isExactlySameType(baseType, type2)) { + return true; + } + } + } + return false; } private handleNumericBigintCompare(node: ts.BinaryExpression): void { diff --git a/ets2panda/linter/test/main/incompatible_function.ets b/ets2panda/linter/test/main/incompatible_function.ets index e137fe5ecc7c1d95332b75831e1e407433c031df..687a6bb89ab5743d3bc7da68fe09903c76d1fe00 100644 --- a/ets2panda/linter/test/main/incompatible_function.ets +++ b/ets2panda/linter/test/main/incompatible_function.ets @@ -79,4 +79,16 @@ type FuncTypeNoParams = () => void; let fNoParams: FuncTypeNoParams = () => { return 0; -}; \ No newline at end of file +}; + +class A {} + +class C extends Array {} + +class B { + private arr: Array = []; + + test(): C { + return this.arr; + } +} \ No newline at end of file diff --git a/ets2panda/linter/test/main/incompatible_function.ets.arkts2.json b/ets2panda/linter/test/main/incompatible_function.ets.arkts2.json index da908095d3d48a520457fd8c8455423a513ff7ff..2757f6f80c22bafa79806545a2c7b026468d11c0 100644 --- a/ets2panda/linter/test/main/incompatible_function.ets.arkts2.json +++ b/ets2panda/linter/test/main/incompatible_function.ets.arkts2.json @@ -203,6 +203,26 @@ "suggest": "", "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", "severity": "ERROR" + }, + { + "line": 92, + "column": 12, + "endLine": 92, + "endColumn": 20, + "problem": "IncompationbleFunctionType", + "suggest": "", + "rule": "Stricter assignments into variables of function type (arkts-incompatible-function-types)", + "severity": "ERROR" + }, + { + "line": 92, + "column": 12, + "endLine": 92, + "endColumn": 20, + "problem": "StructuralIdentity", + "suggest": "", + "rule": "Structural typing is not supported (arkts-no-structural-typing)", + "severity": "ERROR" } ] } \ No newline at end of file