From bd043d137283edb1420ab0fdbd85cdfab1704686 Mon Sep 17 00:00:00 2001 From: sefayilmazunal Date: Thu, 17 Jul 2025 14:27:14 +0300 Subject: [PATCH] arkts-class-same-type-prop-with-super implemented Description: arkts-class-same-type-prop-with-super rule implemented Issue: #ICNC16 Signed-off-by: sefayilmazunal --- ets2panda/linter/rule-config.json | 5 +- ets2panda/linter/src/lib/CookBookMsg.ts | 2 + ets2panda/linter/src/lib/FaultAttrs.ts | 1 + ets2panda/linter/src/lib/FaultDesc.ts | 1 + ets2panda/linter/src/lib/Problems.ts | 1 + ets2panda/linter/src/lib/TypeScriptLinter.ts | 121 ++++++++++++++++++ .../derived_class_field_type_matching.ets | 61 +++++++++ ...ed_class_field_type_matching.ets.args.json | 19 +++ ..._class_field_type_matching.ets.arkts2.json | 88 +++++++++++++ ...derived_class_field_type_matching.ets.json | 17 +++ .../test/main/undefined_check_calls.ets.json | 24 ++-- 11 files changed, 326 insertions(+), 14 deletions(-) create mode 100644 ets2panda/linter/test/main/derived_class_field_type_matching.ets create mode 100644 ets2panda/linter/test/main/derived_class_field_type_matching.ets.args.json create mode 100644 ets2panda/linter/test/main/derived_class_field_type_matching.ets.arkts2.json create mode 100644 ets2panda/linter/test/main/derived_class_field_type_matching.ets.json diff --git a/ets2panda/linter/rule-config.json b/ets2panda/linter/rule-config.json index 349cef2c45..8803eec921 100644 --- a/ets2panda/linter/rule-config.json +++ b/ets2panda/linter/rule-config.json @@ -92,7 +92,8 @@ "arkts-interop-js2s-call-js-method", "arkts-interop-js2s-instanceof-js-type", "arkts-interop-js2s-self-addtion-reduction", - "arkts-promise-with-void-type-need-undefined-as-resolve-arg" + "arkts-promise-with-void-type-need-undefined-as-resolve-arg", + "arkts-class-same-type-prop-with-super" ], "ArkUI": [ "arkui-no-!!-bidirectional-data-binding", @@ -150,4 +151,4 @@ "arkts-limited-stdlib-no-concurrent-decorator", "arkts-no-need-stdlib-worker" ] -} \ No newline at end of file +} diff --git a/ets2panda/linter/src/lib/CookBookMsg.ts b/ets2panda/linter/src/lib/CookBookMsg.ts index ec41a079a8..1644718664 100644 --- a/ets2panda/linter/src/lib/CookBookMsg.ts +++ b/ets2panda/linter/src/lib/CookBookMsg.ts @@ -282,6 +282,8 @@ cookBookTag[261] = cookBookTag[262] = 'The makeObserved function is not supported (arkui-no-makeobserved-function)'; cookBookTag[263] = 'The "@Provide" annotation does not support dynamic parameters (arkui-provide-annotation-parameters)'; +cookBookTag[264] = + 'The field types of the subclass and parent class must be the same (arkts-class-same-type-prop-with-super)'; cookBookTag[265] = 'Direct inheritance of interop JS classes is not supported (arkts-interop-js2s-inherit-js-class)'; cookBookTag[266] = 'Direct usage of interop JS objects is not supported (arkts-interop-js2s-traverse-js-instance)'; cookBookTag[267] = 'Direct usage of interop JS functions is not supported (arkts-interop-js2s-js-call-static-function)'; diff --git a/ets2panda/linter/src/lib/FaultAttrs.ts b/ets2panda/linter/src/lib/FaultAttrs.ts index 5c828160f3..be00342fd5 100644 --- a/ets2panda/linter/src/lib/FaultAttrs.ts +++ b/ets2panda/linter/src/lib/FaultAttrs.ts @@ -187,6 +187,7 @@ faultsAttrs[FaultID.EntryAnnotation] = new FaultAttributes(260); faultsAttrs[FaultID.SdkAbilityLifecycleMonitor] = new FaultAttributes(261); faultsAttrs[FaultID.MakeObservedIsNotSupported] = new FaultAttributes(262); faultsAttrs[FaultID.ProvideAnnotation] = new FaultAttributes(263); +faultsAttrs[FaultID.FieldTypeMismatch] = new FaultAttributes(264); faultsAttrs[FaultID.InteropJsObjectInheritance] = new FaultAttributes(265); faultsAttrs[FaultID.InteropJsObjectTraverseJsInstance] = new FaultAttributes(266); faultsAttrs[FaultID.InteropJsObjectCallStaticFunc] = new FaultAttributes(267, ProblemSeverity.WARNING); diff --git a/ets2panda/linter/src/lib/FaultDesc.ts b/ets2panda/linter/src/lib/FaultDesc.ts index ab3cde4719..95cf282a8f 100644 --- a/ets2panda/linter/src/lib/FaultDesc.ts +++ b/ets2panda/linter/src/lib/FaultDesc.ts @@ -191,6 +191,7 @@ faultDesc[FaultID.EntryAnnotation] = '"@Entry" decorator parameter'; faultDesc[FaultID.SdkAbilityLifecycleMonitor] = 'UIAbility of 1.2 needs to be listened by the new StaticAbilityLifecycleCallback'; faultDesc[FaultID.ProvideAnnotation] = '"@Provide" decorator parameter'; +faultDesc[FaultID.FieldTypeMismatch] = 'Mismatch field types'; faultDesc[FaultID.InteropJsObjectInheritance] = 'Interop JS class inheritance'; faultDesc[FaultID.InteropJsObjectTraverseJsInstance] = 'Interop JS object traverse usage'; faultDesc[FaultID.InteropJsObjectCallStaticFunc] = 'Interop JS function usage'; diff --git a/ets2panda/linter/src/lib/Problems.ts b/ets2panda/linter/src/lib/Problems.ts index fbe1bb1413..24779549fc 100644 --- a/ets2panda/linter/src/lib/Problems.ts +++ b/ets2panda/linter/src/lib/Problems.ts @@ -186,6 +186,7 @@ export enum FaultID { EntryAnnotation, SdkAbilityLifecycleMonitor, ProvideAnnotation, + FieldTypeMismatch, UseSharedDeprecated, UseConcurrentDeprecated, MethodInheritRule, diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index 17e696930b..1d8a523e90 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -7756,6 +7756,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { }); this.handleMissingSuperCallInExtendedClass(node); + this.handleFieldTypesMatchingBetweenDerivedAndBaseClass(node); } } @@ -9534,6 +9535,126 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { } } + /** + * Checks that each field in a subclass matches the type of the same-named field + * in its base class. If the subclass field's type is not assignable to the + * base class field type, emit a diagnostic. + */ + private handleFieldTypesMatchingBetweenDerivedAndBaseClass(node: ts.HeritageClause): void { + // Only process "extends" clauses + if (node.token !== ts.SyntaxKind.ExtendsKeyword) { + return; + } + const derivedClass = node.parent; + if (!ts.isClassDeclaration(derivedClass)) { + return; + } + + // Locate the base class declaration + const baseExpr = node.types[0]?.expression; + if (!ts.isIdentifier(baseExpr)) { + return; + } + const baseSym = this.tsUtils.trueSymbolAtLocation(baseExpr); + const baseClassDecl = baseSym?.declarations?.find(ts.isClassDeclaration); + if (!baseClassDecl) { + return; + } + + // Compare each property in the derived class against the base class + for (const member of derivedClass.members) { + if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name) || !member.type) { + continue; + } + const propName = member.name.text; + // Find the first declaration of this property in the base-class chain + const baseProp = this.findPropertyDeclarationInBaseChain(baseClassDecl, propName); + if (!baseProp) { + continue; + } + + // Get the types + const derivedType = this.tsTypeChecker.getTypeAtLocation(member.type); + const baseType = this.tsTypeChecker.getTypeAtLocation(baseProp.type!); + + // If the derived type is not assignable to the base type, report + if (!this.isFieldTypeMatchingBetweenDerivedAndBaseClass(derivedType, baseType)) { + this.incrementCounters(member.name, FaultID.FieldTypeMismatch); + } + } + } + + /** + * Returns true if the union type members of subclass field's type + * exactly match those of the base class field's type (order-insensitive). + * So `number|string` ↔ `string|number` passes, but `number` ↔ `number|string` fails. + */ + private isFieldTypeMatchingBetweenDerivedAndBaseClass(derivedType: ts.Type, baseType: ts.Type): boolean { + // Split union type strings into trimmed member names + const derivedNames = this.tsTypeChecker. + typeToString(derivedType). + split('|'). + map((s) => { + return s.trim(); + }); + const baseNames = this.tsTypeChecker. + typeToString(baseType). + split('|'). + map((s) => { + return s.trim(); + }); + + // Only match if both unions contain exactly the same members + if (derivedNames.length !== baseNames.length) { + return false; + } + return ( + derivedNames.every((name) => { + return baseNames.includes(name); + }) && + baseNames.every((name) => { + return derivedNames.includes(name); + }) + ); + } + + /** + * Recursively searches base classes to find a property declaration + * with the given name and a type annotation. + */ + private findPropertyDeclarationInBaseChain( + classDecl: ts.ClassDeclaration, + propName: string + ): ts.PropertyDeclaration | undefined { + let current: ts.ClassDeclaration | undefined = classDecl; + while (current) { + // Look for the property in this class + const member = current.members.find((m) => { + return ts.isPropertyDeclaration(m) && ts.isIdentifier(m.name) && m.name.text === propName && !!m.type; + }) as ts.PropertyDeclaration | undefined; + if (member) { + return member; + } + + // Move to the next base class if it exists + const extendsClause = current.heritageClauses?.find((c) => { + return c.token === ts.SyntaxKind.ExtendsKeyword; + }); + if (!extendsClause) { + break; + } + const baseExpr = extendsClause.types[0]?.expression; + if (!ts.isIdentifier(baseExpr)) { + break; + } + + const sym = this.tsUtils.trueSymbolAtLocation(baseExpr); + const decl = sym?.declarations?.find(ts.isClassDeclaration); + current = decl; + } + return undefined; + } + /** * Checks for missing super() call in child classes that extend a parent class * with parameterized constructors. If parent class only has parameterized constructors diff --git a/ets2panda/linter/test/main/derived_class_field_type_matching.ets b/ets2panda/linter/test/main/derived_class_field_type_matching.ets new file mode 100644 index 0000000000..2807603e13 --- /dev/null +++ b/ets2panda/linter/test/main/derived_class_field_type_matching.ets @@ -0,0 +1,61 @@ +/* + * 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 { + obj1: number | string = 0.0; + obj2: string = 'hello'; + obj3: number | string | boolean = false; +} + +class B extends A { + obj1: string | number = 0.0; // OK + obj2: string = 'hello'; // OK + obj3: number | string | boolean = false; // OK +} + +class C extends A { + obj1: string = 'hello'; // MISMATCH/ERROR + obj2: number = 0.0; // MISMATCH/ERROR + obj3: number | boolean = false; // MISMATCH/ERROR +} + +class D extends A { + obj1: string | number | boolean = 'hello'; // MISMATCH/ERROR + obj2: number | string = 0.0; // MISMATCH/ERROR +} + +class E { obj: number | string = 1.0; } +class F extends E { obj: number = 1.0; } // will be flagged on F vs E => MISMATCH/ERROR +class G extends F { obj: number = 1.0; } // will be checked on G vs F => OK + +class A1 {} +class A2 extends A1 { + obj: number | string = 0.0; +} +class A3 extends A2 {} +class A4 extends A3 { + obj: number = 0.0; // ERROR/MISMATCH => field type is not matching with obj from base class A2 +} + +class B1 { + obj: number | string = 0.0; +} + +class B2 extends B1 { // no error + obj: number | string = 0.0; + obj2: number | string = 0.0; + obj3: boolean = false; + obj4: string = 'hello'; +} diff --git a/ets2panda/linter/test/main/derived_class_field_type_matching.ets.args.json b/ets2panda/linter/test/main/derived_class_field_type_matching.ets.args.json new file mode 100644 index 0000000000..66fb88f859 --- /dev/null +++ b/ets2panda/linter/test/main/derived_class_field_type_matching.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/derived_class_field_type_matching.ets.arkts2.json b/ets2panda/linter/test/main/derived_class_field_type_matching.ets.arkts2.json new file mode 100644 index 0000000000..e12acd531a --- /dev/null +++ b/ets2panda/linter/test/main/derived_class_field_type_matching.ets.arkts2.json @@ -0,0 +1,88 @@ +{ + "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": 29, + "column": 3, + "endLine": 29, + "endColumn": 7, + "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": 30, + "column": 3, + "endLine": 30, + "endColumn": 7, + "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": 31, + "column": 3, + "endLine": 31, + "endColumn": 7, + "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": 35, + "column": 3, + "endLine": 35, + "endColumn": 7, + "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": 36, + "column": 3, + "endLine": 36, + "endColumn": 7, + "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": 40, + "column": 21, + "endLine": 40, + "endColumn": 24, + "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": 49, + "column": 3, + "endLine": 49, + "endColumn": 6, + "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" + } + ] +} diff --git a/ets2panda/linter/test/main/derived_class_field_type_matching.ets.json b/ets2panda/linter/test/main/derived_class_field_type_matching.ets.json new file mode 100644 index 0000000000..dd03fcf544 --- /dev/null +++ b/ets2panda/linter/test/main/derived_class_field_type_matching.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": [] +} diff --git a/ets2panda/linter/test/main/undefined_check_calls.ets.json b/ets2panda/linter/test/main/undefined_check_calls.ets.json index 5f20af3a44..2085f45b05 100644 --- a/ets2panda/linter/test/main/undefined_check_calls.ets.json +++ b/ets2panda/linter/test/main/undefined_check_calls.ets.json @@ -1,6 +1,6 @@ { "copyright": [ - "Copyright (c) 2023-2024 Huawei Device Co., Ltd.", + "Copyright (c) 2023-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", @@ -30,8 +30,8 @@ "endLine": 51, "endColumn": 60, "problem": "StrictDiagnostic", - "suggest": "Argument of type 'string | undefined' is not assignable to parameter of type 'ResourceStr'.", - "rule": "Argument of type 'string | undefined' is not assignable to parameter of type 'ResourceStr'.", + "suggest": "Argument of type 'string | undefined' is not assignable to parameter of type 'ResourceStr'.\n Type 'undefined' is not assignable to type 'ResourceStr'.", + "rule": "Argument of type 'string | undefined' is not assignable to parameter of type 'ResourceStr'.\n Type 'undefined' is not assignable to type 'ResourceStr'.", "severity": "ERROR" }, { @@ -100,8 +100,8 @@ "endLine": 92, "endColumn": 15, "problem": "StrictDiagnostic", - "suggest": "Type 'TestClassC' is not assignable to type 'TestClassD'.", - "rule": "Type 'TestClassC' is not assignable to type 'TestClassD'.", + "suggest": "Type 'TestClassC' is not assignable to type 'TestClassD'.\n Types of property 'ccc' are incompatible.\n Type 'null' is not assignable to type 'string'.", + "rule": "Type 'TestClassC' is not assignable to type 'TestClassD'.\n Types of property 'ccc' are incompatible.\n Type 'null' is not assignable to type 'string'.", "severity": "WARNING" }, { @@ -120,8 +120,8 @@ "endLine": 99, "endColumn": 15, "problem": "StrictDiagnostic", - "suggest": "Type 'TestClassC' is not assignable to type 'TestClassD'.", - "rule": "Type 'TestClassC' is not assignable to type 'TestClassD'.", + "suggest": "Type 'TestClassC' is not assignable to type 'TestClassD'.\n Types of property 'ccc' are incompatible.\n Type 'null' is not assignable to type 'string'.", + "rule": "Type 'TestClassC' is not assignable to type 'TestClassD'.\n Types of property 'ccc' are incompatible.\n Type 'null' is not assignable to type 'string'.", "severity": "ERROR" }, { @@ -130,8 +130,8 @@ "endLine": 105, "endColumn": 15, "problem": "StrictDiagnostic", - "suggest": "Type 'TestClassC' is not assignable to type 'TestClassD'.", - "rule": "Type 'TestClassC' is not assignable to type 'TestClassD'.", + "suggest": "Type 'TestClassC' is not assignable to type 'TestClassD'.\n Types of property 'ccc' are incompatible.\n Type 'null' is not assignable to type 'string'.", + "rule": "Type 'TestClassC' is not assignable to type 'TestClassD'.\n Types of property 'ccc' are incompatible.\n Type 'null' is not assignable to type 'string'.", "severity": "ERROR" }, { @@ -140,9 +140,9 @@ "endLine": 106, "endColumn": 15, "problem": "StrictDiagnostic", - "suggest": "Type 'TestClassC' is not assignable to type 'TestClassD'.", - "rule": "Type 'TestClassC' is not assignable to type 'TestClassD'.", + "suggest": "Type 'TestClassC' is not assignable to type 'TestClassD'.\n Types of property 'ccc' are incompatible.\n Type 'null' is not assignable to type 'string'.", + "rule": "Type 'TestClassC' is not assignable to type 'TestClassD'.\n Types of property 'ccc' are incompatible.\n Type 'null' is not assignable to type 'string'.", "severity": "ERROR" } ] -} \ No newline at end of file +} -- Gitee