diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index a15641503fd7aa6af889b0b1b95a5af54dd1809e..3526b386159d5d0d6ae305b195abb4c8b4075108 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -2113,7 +2113,8 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { private handlePostfixUnaryExpression(node: ts.Node): void { const unaryExpr = node as ts.PostfixUnaryExpression; - if (unaryExpr.operator === ts.SyntaxKind.PlusPlusToken || unaryExpr.operator === ts.SyntaxKind.MinusMinusToken) { + if (unaryExpr.operator === ts.SyntaxKind.PlusPlusToken || + unaryExpr.operator === ts.SyntaxKind.MinusMinusToken) { this.checkAutoIncrementDecrement(unaryExpr); } } @@ -4098,7 +4099,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { this.tsUtils.isOrDerivedFrom(type, this.tsUtils.isStdRecordType) || this.tsUtils.isOrDerivedFrom(type, this.tsUtils.isStringType) || !this.options.arkts2 && - (this.tsUtils.isOrDerivedFrom(type, this.tsUtils.isStdMapType) || TsUtils.isIntrinsicObjectType(type)) || + (this.tsUtils.isOrDerivedFrom(type, this.tsUtils.isStdMapType) || TsUtils.isIntrinsicObjectType(type)) || TsUtils.isEnumType(type) || // we allow EsObject here beacuse it is reported later using FaultId.EsObjectType TsUtils.isEsValueType(typeNode) @@ -6194,7 +6195,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { if ( this.compatibleSdkVersion > SENDBALE_FUNCTION_START_VERSION || this.compatibleSdkVersion === SENDBALE_FUNCTION_START_VERSION && - !SENDABLE_FUNCTION_UNSUPPORTED_STAGES_IN_API12.includes(this.compatibleSdkVersionStage) + !SENDABLE_FUNCTION_UNSUPPORTED_STAGES_IN_API12.includes(this.compatibleSdkVersionStage) ) { return true; } @@ -6390,10 +6391,10 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { const typeText = this.tsTypeChecker.typeToString(t); return Boolean( t.flags & ts.TypeFlags.StringLike || - typeText === 'String' || - t.flags & ts.TypeFlags.NumberLike && (/^\d+$/).test(typeText) || - isLiteralInitialized && !hasExplicitTypeAnnotation && !isFloatLiteral || - t.flags & ts.TypeFlags.EnumLike + typeText === 'String' || + t.flags & ts.TypeFlags.NumberLike && (/^\d+$/).test(typeText) || + isLiteralInitialized && !hasExplicitTypeAnnotation && !isFloatLiteral || + t.flags & ts.TypeFlags.EnumLike ); }; @@ -6665,9 +6666,9 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { } if ( this.tsUtils.isOrDerivedFrom(lhsType, this.tsUtils.isArray) && - this.tsUtils.isOrDerivedFrom(rhsType, TsUtils.isTuple) || + this.tsUtils.isOrDerivedFrom(rhsType, TsUtils.isTuple) || this.tsUtils.isOrDerivedFrom(rhsType, this.tsUtils.isArray) && - this.tsUtils.isOrDerivedFrom(lhsType, TsUtils.isTuple) + this.tsUtils.isOrDerivedFrom(lhsType, TsUtils.isTuple) ) { this.incrementCounters(node, FaultID.NoTuplesArrays); } @@ -9039,10 +9040,10 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { private shouldIncrementCounters(node: ts.ElementAccessExpression): boolean { const indexExpr = node.argumentExpression; if (!indexExpr) { - return false; + return false; } if (ts.isStringLiteral(indexExpr) || ts.isNumericLiteral(indexExpr)) { - return true; + return true; } const type = this.tsTypeChecker.getTypeAtLocation(indexExpr); const typeString = this.tsTypeChecker.typeToString(type); @@ -9331,78 +9332,114 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { if (!this.options.arkts2) { return; } + const className = classDecl.name?.getText(); - classDecl.members.forEach((member) => { - if (ts.isMethodDeclaration(member)) { - this.checkMethod(member, className); + const { staticProps, instanceProps } = this.collectClassProperties(classDecl); + + classDecl.members.forEach(member => { + if (!ts.isMethodDeclaration(member) || !member.body) { + return; } + + const methodReturnType = this.tsTypeChecker.getTypeAtLocation(member); + this.checkMethodAndReturnStatements(member.body, className, methodReturnType, staticProps, instanceProps); }); } - private checkMethod(methodNode: ts.MethodDeclaration, className: string | undefined): void { - const variableDeclarations = new Map(); - const returnStatements: ts.ReturnStatement[] = []; - if (methodNode.body) { - ts.forEachChild(methodNode.body, (node) => { - this.visitMethodBody(node, variableDeclarations, returnStatements); - }); - } + private checkMethodAndReturnStatements( + body: ts.Block, + className: string | undefined, + methodReturnType: ts.Type, + staticProps: Map, + instanceProps: Map + ): void { + body.forEachChild((node) => { + if (!ts.isReturnStatement(node) || !node.expression) { + return; + } - const isStaticPropertyAccess = (node: ts.Expression, className: string): boolean => { - return ( - ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === className - ); - }; + const isStaticPropertyAccess = (node: ts.Expression, className: string): boolean => { + return ( + ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === className + ); + }; + const isInstancePropertyAccess = (node: ts.Expression): boolean => { + return ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword; + }; - const isInstancePropertyAccess = (node: ts.Expression): boolean => { - return ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword; - }; + if (className && isStaticPropertyAccess(node.expression, className)) { + this.checkPropertyAccess( + node, + node.expression as ts.PropertyAccessExpression, + staticProps, + methodReturnType + ); + return; + } - this.checkReturnStatements(returnStatements, className, isStaticPropertyAccess, isInstancePropertyAccess); + if (isInstancePropertyAccess(node.expression)) { + this.checkPropertyAccess( + node, + node.expression as ts.PropertyAccessExpression, + instanceProps, + methodReturnType + ); + } + }); } - private visitMethodBody( - node: ts.Node, - variableDeclarations: Map, - returnStatements: ts.ReturnStatement[] + private checkPropertyAccess( + returnNode: ts.ReturnStatement, + propAccess: ts.PropertyAccessExpression, + propsMap: Map, + methodReturnType: ts.Type ): void { - if (ts.isVariableStatement(node)) { - node.declarationList.declarations.forEach((decl) => { - if (ts.isIdentifier(decl.name)) { - variableDeclarations.set(decl.name.text, decl.type); - } - }); - } + const propName = propAccess.name.getText(); + const propType = propsMap.get(propName); - if (ts.isReturnStatement(node)) { - returnStatements.push(node); + if (propType && this.isExactlySameType(propType, methodReturnType)) { + return; } - ts.forEachChild(node, (child) => { - this.visitMethodBody(child, variableDeclarations, returnStatements); - }); + this.incrementCounters(returnNode, FaultID.NoTsLikeSmartType); } - private checkReturnStatements( - returnStatements: ts.ReturnStatement[], - className: string | undefined, - isStaticPropertyAccess: (node: ts.Expression, className: string) => boolean, - isInstancePropertyAccess: (node: ts.Expression) => boolean - ): void { - returnStatements.forEach((returnStmt) => { - if (!returnStmt.expression) { + private collectClassProperties(classDecl: ts.ClassDeclaration): { + staticProps: Map; + instanceProps: Map; + } { + const result = { + staticProps: new Map(), + instanceProps: new Map() + }; + + classDecl.members.forEach(member => { + if (!ts.isPropertyDeclaration(member)) { return; } - const returnType = this.tsTypeChecker.getTypeAtLocation(returnStmt.expression); - if (className && isStaticPropertyAccess(returnStmt.expression, className) && returnType.isUnion()) { - this.incrementCounters(returnStmt, FaultID.NoTsLikeSmartType); - } + const propName = member.name.getText(); + const propType = this.tsTypeChecker.getTypeAtLocation(member); + const isStatic = member.modifiers?.some(m => { + return m.kind === ts.SyntaxKind.StaticKeyword; + }); - if (isInstancePropertyAccess(returnStmt.expression) && returnType.isUnion()) { - this.incrementCounters(returnStmt, FaultID.NoTsLikeSmartType); + if (isStatic) { + result.staticProps.set(propName, propType); + } else { + result.instanceProps.set(propName, propType); } }); + + return result; + } + + 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 this.tsTypeChecker.typeToString(type1) === this.tsTypeChecker.typeToString(type2); } private handleNumericBigintCompare(node: ts.BinaryExpression): void { diff --git a/ets2panda/linter/src/lib/utils/TsUtils.ts b/ets2panda/linter/src/lib/utils/TsUtils.ts index 3a0f7b32ebe57a38f96c7523576958f26d040654..d661221c01d44d2663a2e287477e0fe46509aaa1 100644 --- a/ets2panda/linter/src/lib/utils/TsUtils.ts +++ b/ets2panda/linter/src/lib/utils/TsUtils.ts @@ -2139,6 +2139,14 @@ export class TsUtils { return callSigns && callSigns.length > 0; } + static getFunctionReturnType(type: ts.Type): ts.Type | null { + const signatures = type.getCallSignatures(); + if (signatures.length === 0) { + return null; + } + return signatures[0].getReturnType(); + } + isStdFunctionType(type: ts.Type): boolean { const sym = type.getSymbol(); return !!sym && sym.getName() === 'Function' && this.isGlobalSymbol(sym); diff --git a/ets2panda/linter/test/main/no_ts_like_smart_type.ets b/ets2panda/linter/test/main/no_ts_like_smart_type.ets index 3a4d7ee28d66e130c6375b18bfdc3baa0a308241..65ad923c301050f146d7977c211fcec62d9f481b 100755 --- a/ets2panda/linter/test/main/no_ts_like_smart_type.ets +++ b/ets2panda/linter/test/main/no_ts_like_smart_type.ets @@ -12,29 +12,53 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// static + +class A { + private static instance:A = new A(); + static get():A { + return A.instance; // + } +} + +class AA { + public static instance?: number; + + getInstance(): number { + if (!AA.instance) { + return 0; + } + return AA.instance; // Error + } +} + class AA1 { - public static instance : number | string; - getInstance(): number { - if (AA1.instance instanceof string) { - return 0; - } - return AA1.instance; // Error + public static instance : Number | String | Object = "smart cast"; + getInstance(): Number { + if (!(AA1.instance instanceof Number)) { + return 0; } + return AA1.instance; // Error + } } class AA2 { - public instance : number | string; - getInstance(): number { - if (this.instance instanceof string) { - return 0; - } - return this.instance; // Error + public instance : Number | String | Object= 'smart cast'; + getInstance(): Number { + if (!(this.instance instanceof Number)) { + return 0; } + return this.instance; // Error + } } -class A { - private static instance:A = new A(); - static get():A { - return A.instance; + +class AA3 { + public instance : number | String | Object = 'string'; + getInstance(): number { + if (this.instance instanceof String) { + return 0; + } else if (this.instance instanceof Object) { + return 1; } + return this.instance; // Error + } } \ No newline at end of file diff --git a/ets2panda/linter/test/main/no_ts_like_smart_type.ets.arkts2.json b/ets2panda/linter/test/main/no_ts_like_smart_type.ets.arkts2.json index 45fcc934258a3b05da4885094e7c46c3f8ed2475..d8dd2482a1d8dee31a6959315d9b86a4b67baf2e 100755 --- a/ets2panda/linter/test/main/no_ts_like_smart_type.ets.arkts2.json +++ b/ets2panda/linter/test/main/no_ts_like_smart_type.ets.arkts2.json @@ -15,63 +15,93 @@ ], "result": [ { - "line": 22, - "column": 9, - "endLine": 22, - "endColumn": 29, + "line": 30, + "column": 5, + "endLine": 30, + "endColumn": 24, "problem": "NoTsLikeSmartType", "suggest": "", "rule": "Smart type differences (arkts-no-ts-like-smart-type)", "severity": "ERROR" }, { - "line": 17, + "line": 28, + "column": 14, + "endLine": 28, + "endColumn": 15, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 40, "column": 5, - "endLine": 17, - "endColumn": 46, - "problem": "ClassstaticInitialization", + "endLine": 40, + "endColumn": 25, + "problem": "NoTsLikeSmartType", "suggest": "", - "rule": "The static property has no initializer (arkts-class-static-initialization)", + "rule": "Smart type differences (arkts-no-ts-like-smart-type)", "severity": "ERROR" }, { - "line": 20, - "column": 20, - "endLine": 20, - "endColumn": 21, + "line": 38, + "column": 14, + "endLine": 38, + "endColumn": 15, "problem": "NumericSemantics", "suggest": "", "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", "severity": "ERROR" }, { - "line": 32, - "column": 9, - "endLine": 32, - "endColumn": 30, + "line": 50, + "column": 5, + "endLine": 50, + "endColumn": 26, "problem": "NoTsLikeSmartType", "suggest": "", "rule": "Smart type differences (arkts-no-ts-like-smart-type)", "severity": "ERROR" }, { - "line": 30, - "column": 20, - "endLine": 30, - "endColumn": 21, + "line": 48, + "column": 14, + "endLine": 48, + "endColumn": 15, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 62, + "column": 5, + "endLine": 62, + "endColumn": 26, + "problem": "NoTsLikeSmartType", + "suggest": "", + "rule": "Smart type differences (arkts-no-ts-like-smart-type)", + "severity": "ERROR" + }, + { + "line": 58, + "column": 14, + "endLine": 58, + "endColumn": 15, "problem": "NumericSemantics", "suggest": "", "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", "severity": "ERROR" }, { - "line": 27, - "column": 12, - "endLine": 27, - "endColumn": 20, - "problem": "StrictDiagnostic", - "suggest": "Property 'instance' has no initializer and is not definitely assigned in the constructor.", - "rule": "Property 'instance' has no initializer and is not definitely assigned in the constructor.", + "line": 60, + "column": 14, + "endLine": 60, + "endColumn": 15, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", "severity": "ERROR" } ] diff --git a/ets2panda/linter/test/main/no_ts_like_smart_type.ets.json b/ets2panda/linter/test/main/no_ts_like_smart_type.ets.json index b9d40bdd285aa64170a36db11902a97e7d2644a9..ca88f857e960b437dcf767c0ac40be998c8f1236 100755 --- a/ets2panda/linter/test/main/no_ts_like_smart_type.ets.json +++ b/ets2panda/linter/test/main/no_ts_like_smart_type.ets.json @@ -13,16 +13,5 @@ "See the License for the specific language governing permissions and", "limitations under the License." ], - "result": [ - { - "line": 27, - "column": 12, - "endLine": 27, - "endColumn": 20, - "problem": "StrictDiagnostic", - "suggest": "Property 'instance' has no initializer and is not definitely assigned in the constructor.", - "rule": "Property 'instance' has no initializer and is not definitely assigned in the constructor.", - "severity": "ERROR" - } - ] + "result": [] } \ No newline at end of file