diff --git a/ets2panda/linter/rule-config.json b/ets2panda/linter/rule-config.json index e6a8017193e9c249bc88e3f1ca9ccf98f4639d94..688724d2887711d080734a90f5ff25b49d5e1ec1 100644 --- a/ets2panda/linter/rule-config.json +++ b/ets2panda/linter/rule-config.json @@ -13,6 +13,7 @@ "arkts-no-func-bind", "arkts-no-function-return-this", "arkts-limited-stdlib", + "arkts-no-class-add-super-prop-with-readonly", "arkts-no-classes-as-obj", "arkts-obj-literal-props", "arkts-optional-methods", diff --git a/ets2panda/linter/src/lib/CookBookMsg.ts b/ets2panda/linter/src/lib/CookBookMsg.ts index a99f72046c0ea148319576a5c3979911e5a9ed05..7f903fbd063425a88a2899dc4ecbe6924b2f8a7b 100644 --- a/ets2panda/linter/src/lib/CookBookMsg.ts +++ b/ets2panda/linter/src/lib/CookBookMsg.ts @@ -178,7 +178,8 @@ cookBookTag[144] = 'Usage of standard library is restricted (arkts-limited-stdli cookBookTag[145] = 'Strict type checking is enforced (arkts-strict-typing)'; cookBookTag[146] = 'Switching off type checks with in-place comments is not allowed (arkts-strict-typing-required)'; cookBookTag[147] = 'No dependencies on TypeScript code are currently allowed (arkts-no-ts-deps)'; -cookBookTag[148] = ''; +cookBookTag[148] = + 'Overriding with "readonly" field is not allowed when base field is not "readonly" (arkts-no-class-add-super-prop-with-readonly)'; 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 \'ESValue\' type is restricted (arkts-limited-esobj)'; diff --git a/ets2panda/linter/src/lib/FaultAttrs.ts b/ets2panda/linter/src/lib/FaultAttrs.ts index 24902b1ec9be55ebb71d987607070802238dfc54..6a2850681a1e93bf7662bd9829480e891d702775 100644 --- a/ets2panda/linter/src/lib/FaultAttrs.ts +++ b/ets2panda/linter/src/lib/FaultAttrs.ts @@ -106,6 +106,7 @@ faultsAttrs[FaultID.ImportAssertion] = new FaultAttributes(143); faultsAttrs[FaultID.LimitedStdLibApi] = new FaultAttributes(144); faultsAttrs[FaultID.StrictDiagnostic] = new FaultAttributes(145); faultsAttrs[FaultID.ErrorSuppression] = new FaultAttributes(146); +faultsAttrs[FaultID.NoClassSuperPropReadonly] = new FaultAttributes(148); faultsAttrs[FaultID.ClassAsObject] = new FaultAttributes(149, ProblemSeverity.WARNING); faultsAttrs[FaultID.ClassAsObjectError] = new FaultAttributes(149); faultsAttrs[FaultID.ImportAfterStatement] = new FaultAttributes(150); diff --git a/ets2panda/linter/src/lib/FaultDesc.ts b/ets2panda/linter/src/lib/FaultDesc.ts index 25fe395b073c1a37b74217053bc5f3b9b967664a..0cde28004b13ad6acbf147c3fe15f28f7739344c 100644 --- a/ets2panda/linter/src/lib/FaultDesc.ts +++ b/ets2panda/linter/src/lib/FaultDesc.ts @@ -59,6 +59,7 @@ faultDesc[FaultID.PrivateIdentifier] = 'Private identifiers (with "#" prefix)'; faultDesc[FaultID.LocalFunction] = 'Local function declarations'; faultDesc[FaultID.ConditionalType] = 'Conditional type'; faultDesc[FaultID.MappedType] = 'Mapped type'; +faultDesc[FaultID.NoClassSuperPropReadonly] = 'Readonly property overrides non-readonly parent property'; faultDesc[FaultID.NamespaceAsObject] = 'Namespaces used as objects'; faultDesc[FaultID.ClassAsObject] = faultDesc[FaultID.ClassAsObjectError] = 'Class used as object'; faultDesc[FaultID.NonDeclarationInNamespace] = 'Non-declaration statements in namespaces'; diff --git a/ets2panda/linter/src/lib/Problems.ts b/ets2panda/linter/src/lib/Problems.ts index ab24db5928960e58ed088f0fd98e4bb03b95d9b4..e0aa4e453b1ae2cddb270b4d2d08ed10aa8bf59f 100644 --- a/ets2panda/linter/src/lib/Problems.ts +++ b/ets2panda/linter/src/lib/Problems.ts @@ -57,6 +57,7 @@ export enum FaultID { ConditionalType, MappedType, NamespaceAsObject, + NoClassSuperPropReadonly, ClassAsObject, ClassAsObjectError, NonDeclarationInNamespace, diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index 9a0f958dd60d3038f55a62e81d0ad1584875003d..4198bcc0e2df9e1efd8976e02a15d026d7b23659 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -7957,6 +7957,58 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { this.handleMissingSuperCallInExtendedClass(node); this.handleFieldTypesMatchingBetweenDerivedAndBaseClass(node); + this.checkReadonlyOverridesFromBase(node); + } + } + + private checkReadonlyOverridesFromBase(node: ts.HeritageClause): void { + if (!this.options.arkts2) { + return; + } + if (node.token !== ts.SyntaxKind.ExtendsKeyword) { + return; + } + const childClass = node.parent; + const baseTypeNode = node.types[0]; + if (!ts.isClassDeclaration(childClass) || !baseTypeNode) { + return; + } + const baseType = this.tsTypeChecker.getTypeAtLocation(baseTypeNode); + if (!baseType) { + return; + } + const baseProps = baseType.getProperties(); + this.validateReadonlyOverrides(childClass, baseProps); + } + + private validateReadonlyOverrides(childClass: ts.ClassDeclaration, baseProps: ts.Symbol[]): void { + for (const member of childClass.members) { + if (!ts.isPropertyDeclaration(member) || !member.name) { + continue; + } + const isDerivedReadonly = TsUtils.hasModifier(member.modifiers, ts.SyntaxKind.ReadonlyKeyword); + if (!isDerivedReadonly) { + continue; + } + const memberName = ts.isIdentifier(member.name) ? member.name.text : undefined; + if (!memberName) { + continue; + } + const baseProp = baseProps.find((p) => { + return p.name === memberName; + }); + if (!baseProp) { + continue; + } + + const baseDecl = baseProp.valueDeclaration; + if (!baseDecl || !ts.isPropertyDeclaration(baseDecl)) { + continue; + } + const isBaseReadonly = TsUtils.hasModifier(baseDecl.modifiers, ts.SyntaxKind.ReadonlyKeyword); + if (!isBaseReadonly) { + this.incrementCounters(member, FaultID.NoClassSuperPropReadonly); + } } } diff --git a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets new file mode 100644 index 0000000000000000000000000000000000000000..c2c357ee4cb4b59e0d55af1dea0d04b6c3ad3cdc --- /dev/null +++ b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets @@ -0,0 +1,83 @@ +/* + * 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. + */ + +class A { + readonly x: number = 10; +} +class B extends A { + readonly x: number = 20; // legal +} + +class C { + readonly y: string = "base"; +} +class D extends C { + readonly y: string = "child"; // legal +} + +class E { + z: boolean = true; +} +class F extends E { + z: boolean = false; // legal +} + +class G { + readonly foo: number = 42; +} +class H extends G {} // legal + +class M1 { + data: number = 1; +} +class M2 extends M1 { + readonly data: number = 2; // error +} + +class M3 { + version: string = "1.0"; +} +class M4 extends M3 { + readonly version: "2.0" = "2.0"; // error +} + +class M5 { + isReady: boolean = false; +} +class M6 extends M5 { + readonly isReady: true = true; // error +} + +class A1 { + data: number = 1; +} + +class A2 extends A1 { +} + +class A3 extends A2 { + readonly data: number= 2; // error +} + +class B1 { + readonly data: number = 1; +} + +class B2 extends B1 { +} + +class B3 extends B2 { + readonly data: number= 2; // legal +} \ No newline at end of file diff --git a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.args.json b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.args.json new file mode 100644 index 0000000000000000000000000000000000000000..4acc088d1da62353e56ced57f16b342de413cb78 --- /dev/null +++ b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.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": "" + } +} \ No newline at end of file diff --git a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.arkts2.json b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.arkts2.json new file mode 100644 index 0000000000000000000000000000000000000000..0d2d081c9cd812e8e708e3776131d4039c407231 --- /dev/null +++ b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.arkts2.json @@ -0,0 +1,178 @@ +{ + "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": 17, + "column": 24, + "endLine": 17, + "endColumn": 26, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 20, + "column": 24, + "endLine": 20, + "endColumn": 26, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 38, + "column": 26, + "endLine": 38, + "endColumn": 28, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 43, + "column": 18, + "endLine": 43, + "endColumn": 19, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 46, + "column": 3, + "endLine": 46, + "endColumn": 29, + "problem": "NoClassSuperPropReadonly", + "suggest": "", + "rule": "Overriding with \"readonly\" field is not allowed when base field is not \"readonly\" (arkts-no-class-add-super-prop-with-readonly)", + "severity": "ERROR" + }, + { + "line": 46, + "column": 27, + "endLine": 46, + "endColumn": 28, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 53, + "column": 12, + "endLine": 53, + "endColumn": 19, + "problem": "FieldTypeMismatch", + "suggest": "", + "rule": "The field types of the subclass and parent class must be the same (arkts-class-same-type-prop-with-super)", + "severity": "ERROR" + }, + { + "line": 53, + "column": 3, + "endLine": 53, + "endColumn": 35, + "problem": "NoClassSuperPropReadonly", + "suggest": "", + "rule": "Overriding with \"readonly\" field is not allowed when base field is not \"readonly\" (arkts-no-class-add-super-prop-with-readonly)", + "severity": "ERROR" + }, + { + "line": 60, + "column": 12, + "endLine": 60, + "endColumn": 19, + "problem": "FieldTypeMismatch", + "suggest": "", + "rule": "The field types of the subclass and parent class must be the same (arkts-class-same-type-prop-with-super)", + "severity": "ERROR" + }, + { + "line": 60, + "column": 3, + "endLine": 60, + "endColumn": 33, + "problem": "NoClassSuperPropReadonly", + "suggest": "", + "rule": "Overriding with \"readonly\" field is not allowed when base field is not \"readonly\" (arkts-no-class-add-super-prop-with-readonly)", + "severity": "ERROR" + }, + { + "line": 60, + "column": 21, + "endLine": 60, + "endColumn": 25, + "problem": "LimitedLiteralType", + "suggest": "", + "rule": "Literal types are restricted(arkts-limited-literal-types)", + "severity": "ERROR" + }, + { + "line": 64, + "column": 20, + "endLine": 64, + "endColumn": 21, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 71, + "column": 5, + "endLine": 71, + "endColumn": 30, + "problem": "NoClassSuperPropReadonly", + "suggest": "", + "rule": "Overriding with \"readonly\" field is not allowed when base field is not \"readonly\" (arkts-no-class-add-super-prop-with-readonly)", + "severity": "ERROR" + }, + { + "line": 71, + "column": 28, + "endLine": 71, + "endColumn": 29, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 75, + "column": 29, + "endLine": 75, + "endColumn": 30, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + }, + { + "line": 82, + "column": 28, + "endLine": 82, + "endColumn": 29, + "problem": "NumericSemantics", + "suggest": "", + "rule": "Numeric semantics is different for integer values (arkts-numeric-semantic)", + "severity": "ERROR" + } + ] +} \ No newline at end of file diff --git a/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.json b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.json new file mode 100644 index 0000000000000000000000000000000000000000..ca88f857e960b437dcf767c0ac40be998c8f1236 --- /dev/null +++ b/ets2panda/linter/test/main/arkts_no_class_super_prop_readonly.ets.json @@ -0,0 +1,17 @@ +{ + "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": [] +} \ No newline at end of file