From bbe57ff5bc78a309ec63702377e9fa2b589c82de Mon Sep 17 00:00:00 2001 From: sefayilmazunal Date: Mon, 21 Jul 2025 15:41:57 +0300 Subject: [PATCH] opt field in interface now forced to impl Description: arkts-no-class-omit-interface-optional-prop rule added to force classes implement all fields from an interface Issue: #ICO3VQ Signed-off-by: sefayilmazunal --- ets2panda/linter/rule-config.json | 1 + 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 | 94 ++++++++++++++++++- .../deprecatedapi/swiper_api.ets.arkts2.json | 12 ++- .../main/no_class_omit_interface_optional.ets | 81 ++++++++++++++++ ...lass_omit_interface_optional.ets.args.json | 19 ++++ ...ss_omit_interface_optional.ets.arkts2.json | 78 +++++++++++++++ .../no_class_omit_interface_optional.ets.json | 17 ++++ 11 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 ets2panda/linter/test/main/no_class_omit_interface_optional.ets create mode 100644 ets2panda/linter/test/main/no_class_omit_interface_optional.ets.args.json create mode 100644 ets2panda/linter/test/main/no_class_omit_interface_optional.ets.arkts2.json create mode 100644 ets2panda/linter/test/main/no_class_omit_interface_optional.ets.json diff --git a/ets2panda/linter/rule-config.json b/ets2panda/linter/rule-config.json index cb6e602316..d10f8c1294 100644 --- a/ets2panda/linter/rule-config.json +++ b/ets2panda/linter/rule-config.json @@ -53,6 +53,7 @@ "arkts-limited-stdlib-no-setCloneList", "arkts-limited-stdlib-no-setTransferList", "arkts-builtin-object-getOwnPropertyNames", + "arkts-no-class-omit-interface-optional-prop", "arkts-no-sparse-array", "arkts-no-enum-prop-as-type", "arkts-no-ts-like-smart-type", diff --git a/ets2panda/linter/src/lib/CookBookMsg.ts b/ets2panda/linter/src/lib/CookBookMsg.ts index 3d57443dac..06cfbf17c2 100644 --- a/ets2panda/linter/src/lib/CookBookMsg.ts +++ b/ets2panda/linter/src/lib/CookBookMsg.ts @@ -296,6 +296,8 @@ cookBookTag[274] = 'The subclass constructor must call the parent class\'s parametered constructor (arkts-subclass-must-call-super-constructor-with-args)'; cookBookTag[275] = 'The Custom component with custom layout capability needs to add the "@CustomLayout" decorator (arkui-custom-layout-need-add-decorator)'; +cookBookTag[276] = + 'ArkTS 1.2 should implement all fields in the interface in the class (arkts-no-class-omit-interface-optional-prop)'; cookBookTag[281] = '"@Prop" decorator is not supported (arkui-no-prop-decorator)'; cookBookTag[282] = '"@StorageProp" decorator is not supported (arkui-no-storageprop-decorator)'; cookBookTag[283] = '"@LocalStorageProp" decorator is not supported (arkui-no-localstorageprop-decorator)'; diff --git a/ets2panda/linter/src/lib/FaultAttrs.ts b/ets2panda/linter/src/lib/FaultAttrs.ts index a862704a0c..d3f95fac28 100644 --- a/ets2panda/linter/src/lib/FaultAttrs.ts +++ b/ets2panda/linter/src/lib/FaultAttrs.ts @@ -201,6 +201,7 @@ faultsAttrs[FaultID.InteropJsObjectExpandStaticInstance] = new FaultAttributes(2 faultsAttrs[FaultID.InteropJSFunctionInvoke] = new FaultAttributes(270); faultsAttrs[FaultID.MissingSuperCall] = new FaultAttributes(274); faultsAttrs[FaultID.CustomLayoutNeedAddDecorator] = new FaultAttributes(275); +faultsAttrs[FaultID.InterfaceFieldNotImplemented] = new FaultAttributes(276); faultsAttrs[FaultID.PropDecoratorNotSupported] = new FaultAttributes(281); faultsAttrs[FaultID.StoragePropDecoratorNotSupported] = new FaultAttributes(282); faultsAttrs[FaultID.LocalStoragePropDecoratorNotSupported] = new FaultAttributes(283); diff --git a/ets2panda/linter/src/lib/FaultDesc.ts b/ets2panda/linter/src/lib/FaultDesc.ts index 93a31cd080..174fa4c710 100644 --- a/ets2panda/linter/src/lib/FaultDesc.ts +++ b/ets2panda/linter/src/lib/FaultDesc.ts @@ -254,6 +254,7 @@ faultDesc[FaultID.NumericBigintCompare] = 'No Comparison number between bigint'; faultDesc[FaultID.NondecimalBigint] = 'No non decimal'; faultDesc[FaultID.UnsupportOperator] = 'Unsupport operator'; faultDesc[FaultID.CustomLayoutNeedAddDecorator] = 'Custom layout need add decorator'; +faultDesc[FaultID.InterfaceFieldNotImplemented] = 'All fields must be implemented'; faultDesc[FaultID.PropDecoratorNotSupported] = '"@Prop" decorator is not supported'; faultDesc[FaultID.StoragePropDecoratorNotSupported] = '"@StorageProp" decorator is not supported'; faultDesc[FaultID.LocalStoragePropDecoratorNotSupported] = '"@LocalStorageProp" decorator is not supported'; diff --git a/ets2panda/linter/src/lib/Problems.ts b/ets2panda/linter/src/lib/Problems.ts index 649892bfc7..31c4322d2d 100644 --- a/ets2panda/linter/src/lib/Problems.ts +++ b/ets2panda/linter/src/lib/Problems.ts @@ -254,6 +254,7 @@ export enum FaultID { NondecimalBigint, UnsupportOperator, CustomLayoutNeedAddDecorator, + InterfaceFieldNotImplemented, PropDecoratorNotSupported, StoragePropDecoratorNotSupported, LocalStoragePropDecoratorNotSupported, diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index e58892c530..60088c9ba8 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -8119,7 +8119,7 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { this.handleNoDeprecatedApi(expr); } }); - + this.handleInterfaceFieldImplementation(node); this.handleMissingSuperCallInExtendedClass(node); this.handleFieldTypesMatchingBetweenDerivedAndBaseClass(node); this.checkReadonlyOverridesFromBase(node); @@ -8177,6 +8177,98 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { } } + /** + * Ensures classes fully implement all properties from their interfaces. + */ + private handleInterfaceFieldImplementation(clause: ts.HeritageClause): void { + // Only process implements clauses + if (clause.token !== ts.SyntaxKind.ImplementsKeyword) { + return; + } + const classDecl = clause.parent as ts.ClassDeclaration; + if (!ts.isClassDeclaration(classDecl) || !classDecl.name) { + return; + } + + for (const interfaceType of clause.types) { + const expr = interfaceType.expression; + if (!ts.isIdentifier(expr)) { + continue; + } + const sym = this.tsUtils.trueSymbolAtLocation(expr); + const interfaceDecl = sym?.declarations?.find(ts.isInterfaceDeclaration); + if (!interfaceDecl) { + continue; + } + // Gather all inherited interfaces + const allInterfaces = this.getAllInheritedInterfaces(interfaceDecl); + // If the class fails to implement any member, report once and exit + if (!this.classImplementsAllMembers(classDecl, allInterfaces)) { + this.incrementCounters(classDecl.name, FaultID.InterfaceFieldNotImplemented); + return; + } + } + } + + /** + * Recursively collects an interface and all its ancestor interfaces. + */ + private getAllInheritedInterfaces(root: ts.InterfaceDeclaration): ts.InterfaceDeclaration[] { + const collected: ts.InterfaceDeclaration[] = []; + const stack: ts.InterfaceDeclaration[] = [root]; + while (stack.length) { + const current = stack.pop()!; + collected.push(current); + if (!current.heritageClauses) { + continue; + } + for (const clause of current.heritageClauses) { + if (clause.token !== ts.SyntaxKind.ExtendsKeyword) { + continue; + } + for (const typeNode of clause.types) { + const expr = typeNode.expression; + if (!ts.isIdentifier(expr)) { + continue; + } + const sym = this.tsUtils.trueSymbolAtLocation(expr); + const decl = sym?.declarations?.find(ts.isInterfaceDeclaration); + if (decl) { + stack.push(decl); + } + } + } + } + return collected; + } + + /** + * Returns true if the class declaration declares every property or method + * signature from the provided list of interface declarations. + */ + private classImplementsAllMembers(classDecl: ts.ClassDeclaration, interfaces: ts.InterfaceDeclaration[]): boolean { + void this; + + for (const intf of interfaces) { + for (const member of intf.members) { + if ((ts.isPropertySignature(member) || ts.isMethodSignature(member)) && ts.isIdentifier(member.name)) { + const name = member.name.text; + const found = classDecl.members.some((m) => { + return ( + (ts.isPropertyDeclaration(m) || ts.isMethodDeclaration(m)) && + ts.isIdentifier(m.name) && + m.name.text === name + ); + }); + if (!found) { + return false; + } + } + } + } + return true; + } + private isVariableReference(identifier: ts.Identifier): boolean { const symbol = this.tsTypeChecker.getSymbolAtLocation(identifier); return !!symbol && (symbol.flags & ts.SymbolFlags.Variable) !== 0; diff --git a/ets2panda/linter/test/deprecatedapi/swiper_api.ets.arkts2.json b/ets2panda/linter/test/deprecatedapi/swiper_api.ets.arkts2.json index 9aeb20fcec..a169c132ce 100755 --- a/ets2panda/linter/test/deprecatedapi/swiper_api.ets.arkts2.json +++ b/ets2panda/linter/test/deprecatedapi/swiper_api.ets.arkts2.json @@ -24,6 +24,16 @@ "rule": "ArkUI deprecated api check (arkui-no-deprecated-api)", "severity": "ERROR" }, + { + "line": 17, + "column": 7, + "endLine": 17, + "endColumn": 11, + "problem": "InterfaceFieldNotImplemented", + "suggest": "", + "rule": "ArkTS 1.2 should implement all fields in the interface in the class (arkts-no-class-omit-interface-optional-prop)", + "severity": "ERROR" + }, { "line": 18, "column": 15, @@ -295,4 +305,4 @@ "severity": "ERROR" } ] -} \ No newline at end of file +} diff --git a/ets2panda/linter/test/main/no_class_omit_interface_optional.ets b/ets2panda/linter/test/main/no_class_omit_interface_optional.ets new file mode 100644 index 0000000000..6ea8455199 --- /dev/null +++ b/ets2panda/linter/test/main/no_class_omit_interface_optional.ets @@ -0,0 +1,81 @@ +/* + * 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. + */ + +// 1) All fields implemented — no error +interface I1 { + a: number; + b: string; +} +class C1 implements I1 { // ✅ no error + a: number = 0.0; + b: string = ''; +} + +// 2) One field missing — error +interface I2 { + x: boolean; + y: number; + cb(): void; +} +class C2 implements I2 { // ❌ Error: missing `y` + x: boolean = true; +} +class C21 implements I2 { // ❌ Error: missing `cb()` + x: boolean = true; + y: number = 5.0; +} + +// 3) Optional fields are treated as required — error +interface I3 { + foo?: string; + bar?: number; +} +class C3 implements I3 { // ❌ Error: missing both `foo` and `bar` (first missing stops checking) +} + +// 4) Optional field implemented +class C4 implements I3 { // ✅ no error + foo?: string; + bar?: number; +} + +// 5) Interface extends another +interface A5 { + p: string; +} +interface B5 extends A5 { + q: string; +} +class C5 implements B5 { // ✅ no error + p: string = 'hello'; + q: string = 'world'; +} +class C6 implements B5 { // ❌ Error: missing `q` + p: string = 'hello'; +} +class C61 implements B5 { // ❌ Error: missing `p` + q: string = 'hello'; +} + +// 6) Multiple interfaces at once +interface I6a { u: number } +interface I6b { v: boolean } +class C7 implements I6a, I6b { // ✅ no error + u: number = 42.0; + v: boolean = false; +} +class C8 implements I6a, I6b { // ❌ Error: missing `v` + u: number = 42.0; +} diff --git a/ets2panda/linter/test/main/no_class_omit_interface_optional.ets.args.json b/ets2panda/linter/test/main/no_class_omit_interface_optional.ets.args.json new file mode 100644 index 0000000000..66fb88f859 --- /dev/null +++ b/ets2panda/linter/test/main/no_class_omit_interface_optional.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/no_class_omit_interface_optional.ets.arkts2.json b/ets2panda/linter/test/main/no_class_omit_interface_optional.ets.arkts2.json new file mode 100644 index 0000000000..f20f5ff07f --- /dev/null +++ b/ets2panda/linter/test/main/no_class_omit_interface_optional.ets.arkts2.json @@ -0,0 +1,78 @@ +{ + "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": 32, + "column": 7, + "endLine": 32, + "endColumn": 9, + "problem": "InterfaceFieldNotImplemented", + "suggest": "", + "rule": "ArkTS 1.2 should implement all fields in the interface in the class (arkts-no-class-omit-interface-optional-prop)", + "severity": "ERROR" + }, + { + "line": 35, + "column": 7, + "endLine": 35, + "endColumn": 10, + "problem": "InterfaceFieldNotImplemented", + "suggest": "", + "rule": "ArkTS 1.2 should implement all fields in the interface in the class (arkts-no-class-omit-interface-optional-prop)", + "severity": "ERROR" + }, + { + "line": 45, + "column": 7, + "endLine": 45, + "endColumn": 9, + "problem": "InterfaceFieldNotImplemented", + "suggest": "", + "rule": "ArkTS 1.2 should implement all fields in the interface in the class (arkts-no-class-omit-interface-optional-prop)", + "severity": "ERROR" + }, + { + "line": 65, + "column": 7, + "endLine": 65, + "endColumn": 9, + "problem": "InterfaceFieldNotImplemented", + "suggest": "", + "rule": "ArkTS 1.2 should implement all fields in the interface in the class (arkts-no-class-omit-interface-optional-prop)", + "severity": "ERROR" + }, + { + "line": 68, + "column": 7, + "endLine": 68, + "endColumn": 10, + "problem": "InterfaceFieldNotImplemented", + "suggest": "", + "rule": "ArkTS 1.2 should implement all fields in the interface in the class (arkts-no-class-omit-interface-optional-prop)", + "severity": "ERROR" + }, + { + "line": 79, + "column": 7, + "endLine": 79, + "endColumn": 9, + "problem": "InterfaceFieldNotImplemented", + "suggest": "", + "rule": "ArkTS 1.2 should implement all fields in the interface in the class (arkts-no-class-omit-interface-optional-prop)", + "severity": "ERROR" + } + ] +} diff --git a/ets2panda/linter/test/main/no_class_omit_interface_optional.ets.json b/ets2panda/linter/test/main/no_class_omit_interface_optional.ets.json new file mode 100644 index 0000000000..dd03fcf544 --- /dev/null +++ b/ets2panda/linter/test/main/no_class_omit_interface_optional.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": [] +} -- Gitee