diff --git a/ets2panda/linter/lib/CookBookMsg.ts b/ets2panda/linter/lib/CookBookMsg.ts index 20763e1d1f8cb96195ae4aaefc8ece409766f181..5da6269749d18779bc43fe311b22d4f47227a9a1 100644 --- a/ets2panda/linter/lib/CookBookMsg.ts +++ b/ets2panda/linter/lib/CookBookMsg.ts @@ -177,3 +177,5 @@ cookBookTag[149] = 'Classes cannot be used as objects (arkts-no-classes-as-obj)' cookBookTag[150] = '"import" statements after other statements are not allowed (arkts-no-misplaced-imports)'; cookBookTag[151] = 'Usage of "ESObject" type is restricted (arkts-limited-esobj)'; cookBookTag[152] = '"Function.apply", "Function.call" are not supported (arkts-no-func-apply-call)'; +cookBookTag[153] = 'The inheritance for "Sendable" classes is limited (arkts-sendable-class-inheritance)'; +cookBookTag[154] = 'The initialization for "Sendable" objects is limited (arkts-sendable-initialization)'; \ No newline at end of file diff --git a/ets2panda/linter/lib/FaultAttrs.ts b/ets2panda/linter/lib/FaultAttrs.ts index 016d315f18ab689bee26a8efed909803c7531291..f51d91dbcb9d293ffd26217e49c8c289140afc0f 100644 --- a/ets2panda/linter/lib/FaultAttrs.ts +++ b/ets2panda/linter/lib/FaultAttrs.ts @@ -105,3 +105,5 @@ faultsAttrs[FaultID.ClassAsObject] = new FaultAttributes(149, ProblemSeverity.WA faultsAttrs[FaultID.ImportAfterStatement] = new FaultAttributes(150); faultsAttrs[FaultID.EsObjectType] = new FaultAttributes(151, ProblemSeverity.WARNING); faultsAttrs[FaultID.FunctionApplyCall] = new FaultAttributes(152); +faultsAttrs[FaultID.SendableClassInheritance] = new FaultAttributes(153); +faultsAttrs[FaultID.SendableNoObjectorArrayLiteralInitialization] = new FaultAttributes(154); \ No newline at end of file diff --git a/ets2panda/linter/lib/FaultDesc.ts b/ets2panda/linter/lib/FaultDesc.ts index b22f575ab536b75d227be2d0b8162c0e349693d1..48265b491534334cc5a2b06ffc90400dade5eef3 100644 --- a/ets2panda/linter/lib/FaultDesc.ts +++ b/ets2panda/linter/lib/FaultDesc.ts @@ -97,3 +97,6 @@ faultDesc[FaultID.ErrorSuppression] = 'Error suppression annotation'; faultDesc[FaultID.StrictDiagnostic] = 'Strict diagnostic'; faultDesc[FaultID.ImportAfterStatement] = 'Import declaration after other declaration or statement'; faultDesc[FaultID.EsObjectType] = 'Restricted "ESObject" type'; +faultDesc[FaultID.SendableClassInheritance] = 'Sendable class inheritance'; +faultDesc[FaultID.SendableNoObjectorArrayLiteralInitialization] = 'Object literal or array literal is not allowed to \ + initialize a "Sendable" object' \ No newline at end of file diff --git a/ets2panda/linter/lib/Problems.ts b/ets2panda/linter/lib/Problems.ts index b8b43fc6b3532040e690922193b69bf09cf15cd1..a321aaea2da5c70c5d24471806dfe2fbbb2415d1 100644 --- a/ets2panda/linter/lib/Problems.ts +++ b/ets2panda/linter/lib/Problems.ts @@ -94,6 +94,8 @@ export enum FaultID { StrictDiagnostic, ImportAfterStatement, EsObjectType, + SendableClassInheritance, + SendableNoObjectorArrayLiteralInitialization, // this should always be last enum LAST_ID } diff --git a/ets2panda/linter/lib/TypeScriptLinter.ts b/ets2panda/linter/lib/TypeScriptLinter.ts index d43f928ed35ecf775a5bfaf633b01b79e30fed98..a7bec6d99259a852bbceb5b3e855c6d47de414c8 100644 --- a/ets2panda/linter/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/lib/TypeScriptLinter.ts @@ -815,13 +815,8 @@ export class TypeScriptLinter { // Filter out non-initializable property decorators from strict diagnostics. if (this.tscStrictDiagnostics && this.sourceFile) { if ( - decorators?.some((x) => { - let decoratorName = ''; - if (ts.isIdentifier(x.expression)) { - decoratorName = x.expression.text; - } else if (ts.isCallExpression(x.expression) && ts.isIdentifier(x.expression.expression)) { - decoratorName = x.expression.expression.text; - } + decorators?.some((decorator) => { + const decoratorName = TsUtils.getDecoratorName(decorator); // special case for property of type CustomDialogController of the @CustomDialog-decorated class if (expectedDecorators.includes(NON_INITIALIZABLE_PROPERTY_CLASS_DECORATORS[0])) { return expectedDecorators.includes(decoratorName) && propType === 'CustomDialogController'; @@ -1223,6 +1218,11 @@ export class TypeScriptLinter { if (this.tsUtils.needToDeduceStructuralIdentity(tsVarType, tsInitType, tsVarInit)) { this.incrementCounters(tsVarDecl, FaultID.StructuralIdentity); } + + if (this.tsUtils.isSendableType(tsVarType) && (ts.isArrayLiteralExpression(tsVarInit) || + ts.isObjectLiteralExpression(tsVarInit))) { + this.incrementCounters(tsVarDecl, FaultID.SendableNoObjectorArrayLiteralInitialization); + } } this.handleEsObjectDelaration(tsVarDecl); this.handleDeclarationInferredType(tsVarDecl); @@ -1313,6 +1313,28 @@ export class TypeScriptLinter { } } + private checkFieldsOfSendableClass(element: ts.ClassElement): void { + if (element.name && ts.isComputedPropertyName(element.name)) { + this.incrementCounters((element.name) as ts.ComputedPropertyName, FaultID.ComputedPropertyName); + } + + if (ts.isPropertyDeclaration(element)) { + if (!element.type) { + return; + } + + const elementType: ts.Type = this.tsTypeChecker.getTypeFromTypeNode(element.type); + if (!this.tsUtils.isSendableType(elementType)) { + // TODO report error here + } + let field: ts.PropertyDeclaration = element as ts.PropertyDeclaration; + if (field.initializer && (ts.isObjectLiteralExpression(field.initializer) || + ts.isArrayLiteralExpression(field.initializer))) { + this.incrementCounters(field.initializer, FaultID.SendableNoObjectorArrayLiteralInitialization); + } + } + } + private handleClassDeclaration(node: ts.Node): void { // early exit via exception if cancellation was requested this.cancellationToken?.throwIfCancellationRequested(); @@ -1323,11 +1345,19 @@ export class TypeScriptLinter { } this.countClassMembersWithDuplicateName(tsClassDecl); + const isSendableClass = TsUtils.hasSendableDecorator(tsClassDecl); const visitHClause = (hClause: ts.HeritageClause): void => { for (const tsTypeExpr of hClause.types) { - const tsExprType = this.tsTypeChecker.getTypeAtLocation(tsTypeExpr.expression); - if (tsExprType.isClass() && hClause.token === ts.SyntaxKind.ImplementsKeyword) { - this.incrementCounters(tsTypeExpr, FaultID.ImplementsClass); + const tsExprType = TsUtils.reduceReference(this.tsTypeChecker.getTypeAtLocation(tsTypeExpr)); + if (tsExprType.isClass()) { + if (hClause.token === ts.SyntaxKind.ImplementsKeyword) { + this.incrementCounters(tsTypeExpr, FaultID.ImplementsClass); + } + + const isSendableBaseType = this.tsUtils.isSendableType(tsExprType); + if (isSendableClass && !isSendableBaseType || !isSendableClass && isSendableBaseType) { + this.incrementCounters(tsTypeExpr, FaultID.SendableClassInheritance); + } } } }; @@ -1342,7 +1372,7 @@ export class TypeScriptLinter { } let hasStaticBlock = false; - for (const element of tsClassDecl.members) { + const visitField = (element: ts.ClassElement): void => { if (ts.isClassStaticBlockDeclaration(element)) { if (hasStaticBlock) { this.incrementCounters(element, FaultID.MultipleStaticBlocks); @@ -1350,6 +1380,14 @@ export class TypeScriptLinter { hasStaticBlock = true; } } + + if (isSendableClass) { + this.checkFieldsOfSendableClass(element); + } + } + + for (const element of tsClassDecl.members) { + visitField(element); } } diff --git a/ets2panda/linter/lib/utils/TsUtils.ts b/ets2panda/linter/lib/utils/TsUtils.ts index bb43587435b683ecf1c1b7ad3b02bc24c8084a83..861f80eb14b1b171ebcae5f36b919d9500de14df 100644 --- a/ets2panda/linter/lib/utils/TsUtils.ts +++ b/ets2panda/linter/lib/utils/TsUtils.ts @@ -234,7 +234,7 @@ export class TsUtils { } // does something similar to relatedByInheritanceOrIdentical function - isOrDerivedFrom(tsType: ts.Type, checkType: CheckType): boolean { + isOrDerivedFrom(tsType: ts.Type, checkType: CheckType, checkedBaseTypes?: Set): boolean { tsType = TsUtils.reduceReference(tsType); if (checkType.call(this, tsType)) { @@ -245,6 +245,9 @@ export class TsUtils { return false; } + // Avoid type recursion in heritage by caching checked types. + (checkedBaseTypes ||= new Set()).add(tsType); + for (const tsTypeDecl of tsType.symbol.declarations) { const isClassOrInterfaceDecl = ts.isClassDeclaration(tsTypeDecl) || ts.isInterfaceDeclaration(tsTypeDecl); const isDerived = isClassOrInterfaceDecl && !!tsTypeDecl.heritageClauses; @@ -252,7 +255,7 @@ export class TsUtils { continue; } for (const heritageClause of tsTypeDecl.heritageClauses) { - if (this.processParentTypesCheck(heritageClause.types, checkType)) { + if (this.processParentTypesCheck(heritageClause.types, checkType, checkedBaseTypes)) { return true; } } @@ -584,10 +587,18 @@ export class TsUtils { return false; } - private processParentTypesCheck(parentTypes: ts.NodeArray, checkType: CheckType): boolean { + private processParentTypesCheck( + parentTypes: ts.NodeArray, + checkType: CheckType, + checkedBaseTypes: Set + ): boolean { for (const baseTypeExpr of parentTypes) { const baseType = TsUtils.reduceReference(this.tsTypeChecker.getTypeAtLocation(baseTypeExpr)); - if (baseType && this.isOrDerivedFrom(baseType, checkType)) { + if ( + baseType && + !checkedBaseTypes.has(baseType) && + this.isOrDerivedFrom(baseType, checkType, checkedBaseTypes) + ) { return true; } } @@ -1672,4 +1683,47 @@ export class TsUtils { return false; }); } + + static getDecoratorName(decorator: ts.Decorator): string { + let decoratorName = ''; + if (ts.isIdentifier(decorator.expression)) { + decoratorName = decorator.expression.text; + } else if (ts.isCallExpression(decorator.expression) && ts.isIdentifier(decorator.expression.expression)) { + decoratorName = decorator.expression.expression.text; + } + return decoratorName; + } + + isSendableType(type: ts.Type): boolean { + const sym = type.getSymbol(); + if (!sym) { + return false; + } + + // class with @Sendable decorator + if (type.isClass()) { + if (sym.declarations?.length) { + const decl = sym.declarations[0]; + if (ts.isClassDeclaration(decl) && TsUtils.hasSendableDecorator(decl)) { + return true; + } + } + } + + // ISendable interface, or a class/interface that implements/extends ISendable interface + return this.isOrDerivedFrom(type, TsUtils.isISendableInterface); + } + + static hasSendableDecorator(decl: ts.ClassDeclaration): boolean { + const decorators = ts.getDecorators(decl); + return decorators !== undefined && decorators.some((x) => { + return TsUtils.getDecoratorName(x) === 'Sendable'; + }); + } + + static isISendableInterface(type: ts.Type): boolean { + const sym = type.getSymbol(); + return sym !== undefined && TsUtils.isObjectType(type) && (type.objectFlags & ts.ObjectFlags.Interface) !== 0 && + sym.name === 'ISendable'; + } } diff --git a/ets2panda/linter/test/sendable_class_inheritance.ts b/ets2panda/linter/test/sendable_class_inheritance.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef11032842401cbe946f49e6d35a19004123b7e9 --- /dev/null +++ b/ets2panda/linter/test/sendable_class_inheritance.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +let Sendable = (x, y) => {} + +// Sendable class inheritance +class NonSendableClass {} + +@Sendable +class SendableClass {} // OK + +@Sendable +class BadInheritance1 extends NonSendableClass {} // ERROR, extends non-sendable + +class BadInheritance2 extends SendableClass {} // ERROR, no @Sendable decorator + +@Sendable +class GoodInheritance extends SendableClass {} // OK + +// Implement ISendable interface +interface ISendable {} + +class SendableImpl implements ISendable {} // OK, class is now sendable + +class BadInterfaceImpl extends SendableImpl {} // ERROR, no @Sendable decorator + +@Sendable +class GoodInterfaceImpl extends SendableImpl {} // OK + +// Implement interface extending ISendable +interface ISendableExt extends ISendable {} + +class GoodInterfaceExtImpl implements ISendableExt {} // OK, class implements interface that extends ISendable diff --git a/ets2panda/linter/test/sendable_class_inheritance.ts.autofix.json b/ets2panda/linter/test/sendable_class_inheritance.ts.autofix.json new file mode 100644 index 0000000000000000000000000000000000000000..122a08fed0d9b9882fa5f1b5f82b9a230c972de2 --- /dev/null +++ b/ets2panda/linter/test/sendable_class_inheritance.ts.autofix.json @@ -0,0 +1,55 @@ +{ + "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." + ], + "nodes": [ + { + "line": 16, + "column": 17, + "problem": "AnyType", + "autofixable": false, + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)" + }, + { + "line": 16, + "column": 20, + "problem": "AnyType", + "autofixable": false, + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)" + }, + { + "line": 25, + "column": 31, + "problem": "SendableClassInheritance", + "autofixable": false, + "rule": "The inheritance for \"Sendable\" classes is limited (arkts-sendable-class-inheritance)" + }, + { + "line": 27, + "column": 31, + "problem": "SendableClassInheritance", + "autofixable": false, + "rule": "The inheritance for \"Sendable\" classes is limited (arkts-sendable-class-inheritance)" + }, + { + "line": 37, + "column": 32, + "problem": "SendableClassInheritance", + "autofixable": false, + "rule": "The inheritance for \"Sendable\" classes is limited (arkts-sendable-class-inheritance)" + } + ] +} \ No newline at end of file diff --git a/ets2panda/linter/test/sendable_class_inheritance.ts.json b/ets2panda/linter/test/sendable_class_inheritance.ts.json new file mode 100644 index 0000000000000000000000000000000000000000..524b6b0096aa7536f38a95b76ed3e97349d60567 --- /dev/null +++ b/ets2panda/linter/test/sendable_class_inheritance.ts.json @@ -0,0 +1,50 @@ +{ + "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." + ], + "nodes": [ + { + "line": 16, + "column": 17, + "problem": "AnyType", + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)" + }, + { + "line": 16, + "column": 20, + "problem": "AnyType", + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)" + }, + { + "line": 25, + "column": 31, + "problem": "SendableClassInheritance", + "rule": "The inheritance for \"Sendable\" classes is limited (arkts-sendable-class-inheritance)" + }, + { + "line": 27, + "column": 31, + "problem": "SendableClassInheritance", + "rule": "The inheritance for \"Sendable\" classes is limited (arkts-sendable-class-inheritance)" + }, + { + "line": 37, + "column": 32, + "problem": "SendableClassInheritance", + "rule": "The inheritance for \"Sendable\" classes is limited (arkts-sendable-class-inheritance)" + } + ] +} \ No newline at end of file