diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index e61b3a7a635c2b19614ac16a395a48af0fc5a76c..27080107f141c57a1a6b35cbd8b19aa821a2d7dc 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -10765,59 +10765,166 @@ private checkOnClickCallback(tsCallOrNewExpr: ts.CallExpression | ts.NewExpressi /** * 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. + * in its base class or implemented interfaces. */ private handleFieldTypesMatchingBetweenDerivedAndBaseClass(node: ts.HeritageClause): void { - // Only process "extends" clauses - if (node.token !== ts.SyntaxKind.ExtendsKeyword) { + if (node.token !== ts.SyntaxKind.ExtendsKeyword && node.token !== ts.SyntaxKind.ImplementsKeyword) { 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) { + + // Delegate heritage comparison logic + if (this.hasFieldTypeMismatchWithBases(node, propName, member)) { + this.incrementCounters(member.name, FaultID.FieldTypeMismatch); + } + } + } + + /** + * Checks the given derived property against all base classes/interfaces + * in the heritage clause. Returns true if a mismatch is found. + */ + private hasFieldTypeMismatchWithBases( + node: ts.HeritageClause, + propName: string, + member: ts.PropertyDeclaration + ): boolean { + for (const hType of node.types) { + const baseExpr = hType?.expression; + if (!ts.isIdentifier(baseExpr)) { continue; } - // Get the types - const derivedType = this.tsTypeChecker.getTypeAtLocation(member.type); - const baseType = this.tsTypeChecker.getTypeAtLocation(baseProp.type!); + const baseSym = this.tsUtils.trueSymbolAtLocation(baseExpr); + const baseDecl = baseSym?.declarations?.find(TsUtils.isClassOrInterfaceDeclaration); + if (!baseDecl) { + continue; + } - // If the derived type is not assignable to the base type, report - if (!this.isFieldTypeMatchingBetweenDerivedAndBaseClass(derivedType, baseType)) { - this.incrementCounters(member.name, FaultID.FieldTypeMismatch); + const baseProp = this.findPropertyDeclarationInBaseChain(baseDecl, propName); + if (!baseProp?.type) { + continue; + } + + const derivedType = this.tsTypeChecker.getTypeAtLocation(member.type!); + const baseType = this.tsTypeChecker.getTypeAtLocation(baseProp.type); + + if (!this.isFieldTypeMatchingBetweenDerivedAndBase(derivedType, baseType)) { + return true; + } + } + return false; + } + + /** + * Searches the base chain (classes or interfaces) to find the first declaration + * of the given property (with a type annotation). Avoids cycles. + */ + private findPropertyDeclarationInBaseChain( + decl: ts.ClassDeclaration | ts.InterfaceDeclaration, + propName: string, + visited: Set = new Set() + ): ts.PropertyDeclaration | ts.PropertySignature | undefined { + if (visited.has(decl)) { + return undefined; + } + visited.add(decl); + + if (ts.isClassDeclaration(decl)) { + return this.findPropertyInClassChain(decl, propName, visited); + } + + // Interface path + return this.findPropertyInInterfaceChain(decl, propName, visited); + } + + /** Look for a property in a class declaration or its base classes */ + private findPropertyInClassChain( + decl: ts.ClassDeclaration, + propName: string, + visited: Set + ): ts.PropertyDeclaration | ts.PropertySignature | undefined { + // Check current class members + const member = decl.members.find((m): m is ts.PropertyDeclaration => { + return ts.isPropertyDeclaration(m) && ts.isIdentifier(m.name) && m.name.text === propName && !!m.type; + }); + if (member) { + return member; + } + + // Otherwise, follow the extends clause (single inheritance) + const ext = decl.heritageClauses?.find((c) => { + return c.token === ts.SyntaxKind.ExtendsKeyword; + }); + if (!ext || ext.types.length === 0) { + return undefined; + } + + const expr = ext.types[0].expression; + if (!ts.isIdentifier(expr)) { + return undefined; + } + + const sym = this.tsUtils.trueSymbolAtLocation(expr); + const nextDecl = sym?.declarations?.find(ts.isClassDeclaration); + return nextDecl ? this.findPropertyInClassChain(nextDecl, propName, visited) : undefined; + } + + /** Look for a property in an interface declaration or its extended interfaces */ + private findPropertyInInterfaceChain( + decl: ts.InterfaceDeclaration, + propName: string, + visited: Set + ): ts.PropertySignature | ts.PropertyDeclaration | undefined { + // Check current interface members + const member = decl.members.find((m): m is ts.PropertySignature => { + return ts.isPropertySignature(m) && ts.isIdentifier(m.name) && m.name.text === propName && !!m.type; + }); + if (member) { + return member; + } + + // Otherwise, follow extended interfaces + const ext = decl.heritageClauses?.find((c) => { + return c.token === ts.SyntaxKind.ExtendsKeyword; + }); + if (!ext) { + return undefined; + } + + for (const t of ext.types) { + const expr = t.expression; + if (!ts.isIdentifier(expr)) { + continue; + } + const sym = this.tsUtils.trueSymbolAtLocation(expr); + const nextDecl = sym?.declarations?.find(ts.isInterfaceDeclaration); + if (nextDecl) { + const found = this.findPropertyInInterfaceChain(nextDecl, propName, visited); + if (found) { + return found; + } } } + return undefined; } /** * Returns true if the union type members of subclass field's type - * exactly match those of the base class field's type (order-insensitive). + * exactly match those of the base 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 { + private isFieldTypeMatchingBetweenDerivedAndBase(derivedType: ts.Type, baseType: ts.Type): boolean { // Split union type strings into trimmed member names const derivedNames = this.tsTypeChecker. typeToString(derivedType). @@ -10846,43 +10953,6 @@ private checkOnClickCallback(tsCallOrNewExpr: ts.CallExpression | ts.NewExpressi ); } - /** - * 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; - } - /** * If a class method overrides a base-class abstract method that had no explicit return type, * then any explicit return type other than `void` is an error. diff --git a/ets2panda/linter/src/lib/utils/TsUtils.ts b/ets2panda/linter/src/lib/utils/TsUtils.ts index fb6fd7b7ffdd9e8170ef67f5c74ded4f3285363e..d49c2cdff90b6f373fae3ef111910582e071cbcc 100644 --- a/ets2panda/linter/src/lib/utils/TsUtils.ts +++ b/ets2panda/linter/src/lib/utils/TsUtils.ts @@ -3620,6 +3620,10 @@ export class TsUtils { return !!originalIdentifier && this.isImportedFromJS(originalIdentifier); } + static isClassOrInterfaceDeclaration(d: ts.Declaration): d is ts.ClassDeclaration | ts.InterfaceDeclaration { + return ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d); + } + /** * Extracts the Identifier from the given node. returns undefined if no Identifier is found. * diff --git a/ets2panda/linter/test/deprecatedapi/chip_group_api.ets.arkts2.json b/ets2panda/linter/test/deprecatedapi/chip_group_api.ets.arkts2.json index 667d91d76ded29c2264848d9ce8089452797bddf..57125855d0633cc9a2ff1e8248725d39c800f522 100644 --- a/ets2panda/linter/test/deprecatedapi/chip_group_api.ets.arkts2.json +++ b/ets2panda/linter/test/deprecatedapi/chip_group_api.ets.arkts2.json @@ -14,6 +14,16 @@ "limitations under the License." ], "result": [ + { + "line": 35, + "column": 3, + "endLine": 35, + "endColumn": 13, + "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, diff --git a/ets2panda/linter/test/main/derived_class_field_type_matching.ets b/ets2panda/linter/test/main/derived_class_field_type_matching.ets index 2807603e1334df15008dad76a06d1de1bf0fec72..02bfd0a392cfaea06d5829bdd5af8d55d7fd5091 100644 --- a/ets2panda/linter/test/main/derived_class_field_type_matching.ets +++ b/ets2panda/linter/test/main/derived_class_field_type_matching.ets @@ -59,3 +59,22 @@ class B2 extends B1 { // no error obj3: boolean = false; obj4: string = 'hello'; } + +interface I1 { + obj: number | string +} + +interface I2 { + obj2: number | string; +} + +interface I3 extends I2 {} + +class CI1 implements I1, I2 { + obj: number = 0.0; // error + obj2: number = 0.0; // error +} + +class CI2 implements I3 { + obj2: number = 0.0; // error +} 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 index e12acd531a0c002f49fca591ed20a024ce5aa4b2..06df6cd967e39790e97f60cebc51f6c4963c3f88 100644 --- 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 @@ -1,88 +1,118 @@ { - "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" - } - ] -} + "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" + }, + { + "line": 74, + "column": 3, + "endLine": 74, + "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" + }, + { + "line": 75, + "column": 3, + "endLine": 75, + "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": 79, + "column": 3, + "endLine": 79, + "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" + } + ] +} \ No newline at end of file