diff --git a/ets2panda/linter/rule-config.json b/ets2panda/linter/rule-config.json index ac4230ae61513dfbe0ef105fa68d3d66f6a89ab4..7501f29c2609c9da8f94aaff1630f941c62564d9 100644 --- a/ets2panda/linter/rule-config.json +++ b/ets2panda/linter/rule-config.json @@ -62,6 +62,7 @@ "arkts-no-class-omit-interface-optional-prop", "arkts-distinct-abstract-method-default-return-type", "arkts-class-no-signature-distinct-with-object-public-api", + "arkts-union-assignment-with-obj-literal-ambiguity", "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 0536594a90edf1c2348a8ae1e0525b76c171fa41..81d05de716e5cc170fcc88c7c17e7f35e76863ef 100644 --- a/ets2panda/linter/src/lib/CookBookMsg.ts +++ b/ets2panda/linter/src/lib/CookBookMsg.ts @@ -384,6 +384,8 @@ cookBookTag[351] = 'The taskpool setTransferList interface is deleted from ArkTS1.2 (arkts-limited-stdlib-no-setTransferList)'; cookBookTag[352] = '1.2 Void cannot be combined. OnDestroy/onDisconnect (The return type of the method is now void | Promise) needs to be split into two interfaces. (sdk-ability-asynchronous-lifecycle)'; +cookBookTag[353] = + 'Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)'; cookBookTag[355] = 'Usage of standard library is restricted(arkts-limited-stdlib-no-sendable-decorator)'; cookBookTag[356] = 'Usage of standard library is restricted(arkts-limited-stdlib-no-concurrent-decorator)'; cookBookTag[357] = 'Worker are not supported(arkts-no-need-stdlib-worker)'; diff --git a/ets2panda/linter/src/lib/FaultAttrs.ts b/ets2panda/linter/src/lib/FaultAttrs.ts index a3de17fb3f126dbcff91abe370cd94f3bfa42b00..28a621f7e42de8e063eb854a6eeb1d644877b808 100644 --- a/ets2panda/linter/src/lib/FaultAttrs.ts +++ b/ets2panda/linter/src/lib/FaultAttrs.ts @@ -269,6 +269,7 @@ faultsAttrs[FaultID.SharedArrayBufferDeprecated] = new FaultAttributes(349); faultsAttrs[FaultID.SetCloneListDeprecated] = new FaultAttributes(350); faultsAttrs[FaultID.SetTransferListDeprecated] = new FaultAttributes(351); faultsAttrs[FaultID.SdkAbilityAsynchronousLifecycle] = new FaultAttributes(352); +faultsAttrs[FaultID.ObjectLiteralUnionNeedsCast] = new FaultAttributes(353); faultsAttrs[FaultID.LimitedStdLibNoSendableDecorator] = new FaultAttributes(355); faultsAttrs[FaultID.LimitedStdLibNoDoncurrentDecorator] = new FaultAttributes(356); faultsAttrs[FaultID.NoNeedStdlibWorker] = new FaultAttributes(357); diff --git a/ets2panda/linter/src/lib/FaultDesc.ts b/ets2panda/linter/src/lib/FaultDesc.ts index f6cd393f2ca68fdcc7b129f549c784cd0fadf550..1866e77ffcc8ca46b1261da5e7113d70e6e4e299 100644 --- a/ets2panda/linter/src/lib/FaultDesc.ts +++ b/ets2panda/linter/src/lib/FaultDesc.ts @@ -247,6 +247,7 @@ faultDesc[FaultID.SharedArrayBufferDeprecated] = 'SharedArrayBuffer is not suppo faultDesc[FaultID.SetCloneListDeprecated] = 'setCloneList is not supported'; faultDesc[FaultID.SetTransferListDeprecated] = 'setTransferList is not supported'; faultDesc[FaultID.SdkAbilityAsynchronousLifecycle] = '1.2 Void cannot be combined'; +faultDesc[FaultID.ObjectLiteralUnionNeedsCast] = 'Object literals require union member assertion'; faultDesc[FaultID.LimitedStdLibNoSendableDecorator] = 'Limited stdlib no sendable decorator'; faultDesc[FaultID.LimitedStdLibNoDoncurrentDecorator] = 'Limited stdlib no concurrent decorator'; faultDesc[FaultID.NoNeedStdlibWorker] = 'No need stdlib worker'; diff --git a/ets2panda/linter/src/lib/Problems.ts b/ets2panda/linter/src/lib/Problems.ts index 60422d36c586b99d79146f09aa26b2d5d53a6515..d39588f2fc40443c95e6f97b1ffe36cb08f5afdb 100644 --- a/ets2panda/linter/src/lib/Problems.ts +++ b/ets2panda/linter/src/lib/Problems.ts @@ -246,6 +246,7 @@ export enum FaultID { SetCloneListDeprecated, SetTransferListDeprecated, SdkAbilityAsynchronousLifecycle, + ObjectLiteralUnionNeedsCast, LimitedStdLibNoSendableDecorator, LimitedStdLibNoDoncurrentDecorator, NoNeedStdlibWorker, diff --git a/ets2panda/linter/src/lib/TypeScriptLinter.ts b/ets2panda/linter/src/lib/TypeScriptLinter.ts index 5e7a8387f8fb3cf67c95614c17de1089929f0fd8..56a11b2213c3678ba7ab069e82b88dd0cc4a822e 100644 --- a/ets2panda/linter/src/lib/TypeScriptLinter.ts +++ b/ets2panda/linter/src/lib/TypeScriptLinter.ts @@ -7463,6 +7463,8 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { * 'arkts-no-structural-typing' check was missing in some scenarios, * in order not to cause incompatibility, * only need to strictly match the type of filling the check again + * + * Also delegates the object-literal → union rule to `handleObjectLiteralUnionArg`. */ private checkAssignmentMatching( contextNode: ts.Node, @@ -7471,6 +7473,10 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { isNewStructuralCheck: boolean = false ): void { const rhsType = this.tsTypeChecker.getTypeAtLocation(rhsExpr); + + // Object-literal to union rule (non-call contexts) + this.handleObjectLiteralUnionArg(lhsType, rhsExpr); + this.handleNoTuplesArrays(contextNode, lhsType, rhsType); this.handleArrayTypeImmutable(contextNode, lhsType, rhsType, rhsExpr); // check that 'sendable typeAlias' is assigned correctly @@ -7486,6 +7492,59 @@ export class TypeScriptLinter extends BaseTypeScriptLinter { this.checkFunctionalTypeCompatibility(lhsType, rhsType, rhsExpr); } + /** + * Flags `{ ... }` used where the LHS type is a union with + * more than one non-nullish member and the object literal + * is not already asserted (e.g., `{...} as A`). + * Applies to variable initializers, assignments, call expressions and returns + * that route through `checkAssignmentMatching`. + */ + private handleObjectLiteralUnionArg(lhsType: ts.Type, rhsExpr: ts.Expression): void { + if (!this.options.arkts2) { + return; + } + + if (!ts.isObjectLiteralExpression(rhsExpr) || !lhsType?.isUnion()) { + return; + } + + // Already asserted/cast? Allowed. + const parent = rhsExpr.parent; + if (ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent)) { + return; + } + + // Allow nullish unions like 'T | null | undefined' + const nonNullishMembers = lhsType.types.filter((t) => { + return !TsUtils.isNullishType(t); + }); + if (nonNullishMembers.length <= 1) { + return; + } + + // Skip any types that are from standard lib + const nonStdlibMembers = nonNullishMembers.filter((t) => { + return !(t.getSymbol() && isStdLibrarySymbol(t.getSymbol())); + }); + + const hasClassOrInterfaceMember = nonStdlibMembers.some((t) => { + const sym = t.aliasSymbol ?? t.getSymbol(); + if (!sym) { + return false; + } + const decls = sym.getDeclarations() ?? []; + + return decls.some((d) => { + return ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d); + }); + }); + if (!hasClassOrInterfaceMember) { + return; + } + + this.incrementCounters(rhsExpr, FaultID.ObjectLiteralUnionNeedsCast); + } + private handleStructuralTyping( contextNode: ts.Node, lhsType: ts.Type, diff --git a/ets2panda/linter/src/lib/utils/TsUtils.ts b/ets2panda/linter/src/lib/utils/TsUtils.ts index 1eea1826ef462274b8813ba8b2dd1429d51b203b..1515c5aadb62f214991fac43d6fdbcbff355bf21 100644 --- a/ets2panda/linter/src/lib/utils/TsUtils.ts +++ b/ets2panda/linter/src/lib/utils/TsUtils.ts @@ -481,16 +481,26 @@ export class TsUtils { } static isNullableUnionType(type: ts.Type): boolean { - if (type.isUnion()) { - for (const t of type.types) { - if (!!(t.flags & ts.TypeFlags.Undefined) || !!(t.flags & ts.TypeFlags.Null)) { - return true; - } + if (!type.isUnion()) { + return false; + } + + for (const t of type.types) { + if (TsUtils.isNullishType(t)) { + return true; } } + return false; } + /** + * Returns true if the given type is `null` or `undefined`. + */ + static isNullishType(t: ts.Type): boolean { + return !!(t.flags & ts.TypeFlags.Undefined) || !!(t.flags & ts.TypeFlags.Null); + } + static isMethodAssignment(tsSymbol: ts.Symbol | undefined): boolean { return ( !!tsSymbol && (tsSymbol.flags & ts.SymbolFlags.Method) !== 0 && (tsSymbol.flags & ts.SymbolFlags.Assignment) !== 0 diff --git a/ets2panda/linter/test/interop/object_literal_union_type.ets.arkts2.json b/ets2panda/linter/test/interop/object_literal_union_type.ets.arkts2.json index 4b9be812e45c1e79e4d5a1eab9e65a5f8a05af61..d65993bf6342bb03a32481645d0f26e1b5155ac4 100644 --- a/ets2panda/linter/test/interop/object_literal_union_type.ets.arkts2.json +++ b/ets2panda/linter/test/interop/object_literal_union_type.ets.arkts2.json @@ -14,6 +14,16 @@ "limitations under the License." ], "result": [ + { + "line": 20, + "column": 17, + "endLine": 20, + "endColumn": 31, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, { "line": 20, "column": 5, @@ -24,6 +34,16 @@ "rule": "Object literal not compatible with target union type. (arkts-interop-d2s-object-literal-no-ambiguity)", "severity": "ERROR" }, + { + "line": 22, + "column": 17, + "endLine": 22, + "endColumn": 31, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, { "line": 22, "column": 5, @@ -34,6 +54,16 @@ "rule": "Object literal not compatible with target union type. (arkts-interop-d2s-object-literal-no-ambiguity)", "severity": "ERROR" }, + { + "line": 24, + "column": 22, + "endLine": 24, + "endColumn": 37, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, { "line": 24, "column": 5, @@ -44,6 +74,16 @@ "rule": "Object literal not compatible with target union type. (arkts-interop-d2s-object-literal-no-ambiguity)", "severity": "ERROR" }, + { + "line": 26, + "column": 17, + "endLine": 26, + "endColumn": 34, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, { "line": 26, "column": 5, diff --git a/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets b/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets new file mode 100644 index 0000000000000000000000000000000000000000..6f7975af877e72973302c6af94b3fdb6b8d46d4d --- /dev/null +++ b/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets @@ -0,0 +1,123 @@ +/* + * 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. + */ + +// --------------------- +// Shared types +// --------------------- +interface IA { + value: number; + name?: string; +} + +interface IB { + value: number; +} + +class CA { + value: number = 1; +} + +class CB { + value: number = 1; +} + +// --------------------- +// Allowed Cases +// --------------------- + +// ----------- Allowed: simple type, no union ------------- +let ok1: IA = { value: 1 }; // OK — no union + +// ----------- Allowed: nullish unions -------------------- +let ok2: IA | null = { value: 1 }; // OK — null union +let ok3: IA | undefined = { value: 1 }; // OK — undefined union +let ok4: IA | null | undefined = { value: 1 }; // OK — null + undefined union + +// ----------- Allowed: casted unions ------------------------ +let ok5: IA | IB = { value: 1 } as IA; // OK — explicit cast + +// ----------- Built-in lib.* guard: should be allowed -------------------- +let ok6: Record | object[] = {}; // OK — both are lib.* types +let ok7: Promise | string[] = {}; // OK — Promise and Array are from lib.* + +// ----------- Other allowed scenarios --------------------- + +// Using 'as' cast +function ok8(): IA | CB { + return { value: 1 } as IA; // ok +} + +// Passing nullish or undefined unions — allowed +function ok9(a: IA | null | undefined) {} +ok9({ value: 42 }); // ok + +// Passing a variable declared as IA +const objIA: IA = { value: 1 }; +function ok10(a: IA | IB) { + console.log(a.value); +} +ok10(objIA); // ok + +// --------------------- +// Disallowed Cases +// --------------------- + +// ----------- Disallowed: simple union of interface + interface ------------ +let fail1: IA | IB = { value: 1 }; // ❌ Error — object literal into interface union + +// ----------- Disallowed: interface + class ----------------- +let fail2: IA | CA = { value: 1 }; // ❌ Error + +// ----------- Disallowed: simple union of class + class ------------------ +let fail3: CA | CB = { value: 1 }; // ❌ Error + +// ----------- Disallowed: with nullish in mixed but still failing -------------------- +let fail4: IA | IB | undefined = { value: 1 }; // ❌ Error — nullish doesn’t save it here + +// ----------- Disallowed: function return types --------------------- + +// Return object literal to interface|interface without cast +function err1(): IA | IB { + return { value: 1 }; // ❌ error +} + +// Return object literal to class|interface without cast +function err2(): CB | IA { + return { value: 1 }; // ❌ error +} + +// ----------- Disallowed: function param passing --------------------- + +// Pass object literal to interface|interface param without cast +function err3(a: IA | IB) { + console.log(a.value); +} +err3({ value: 42 }); // ❌ error + +// Pass object literal to class|class param without cast +function err4(a: CA | CB) { + console.log(a.value); +} +err4({ value: 10 }); // ❌ error + +// Mixed interface|class param without cast +function err5(a: IA | CB) { + console.log(a.value); +} +err5({ value: 5 }); // ❌ error + +// Nested in union with undefined — still error +function err6(a: IA | CB | undefined) {} +err6({ value: 99 }); // ❌ error diff --git a/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets.args.json b/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets.args.json new file mode 100644 index 0000000000000000000000000000000000000000..bc4d2071daf6e9354e711c3b74b6be2b56659066 --- /dev/null +++ b/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.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/union_assignment_with_obj_literal_ambiguity.ets.arkts2.json b/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets.arkts2.json new file mode 100644 index 0000000000000000000000000000000000000000..a5200e79a8f6cc8dd2e9208fa0b321c678b811c6 --- /dev/null +++ b/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets.arkts2.json @@ -0,0 +1,138 @@ +{ + "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": 53, + "column": 18, + "endLine": 53, + "endColumn": 21, + "problem": "AnyType", + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)", + "severity": "ERROR" + }, + { + "line": 53, + "column": 36, + "endLine": 53, + "endColumn": 37, + "problem": "ObjectLiteralNoContextType", + "suggest": "", + "rule": "Object literal must correspond to some explicitly declared class or interface (arkts-no-untyped-obj-literals)", + "severity": "ERROR" + }, + { + "line": 78, + "column": 22, + "endLine": 78, + "endColumn": 34, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 81, + "column": 22, + "endLine": 81, + "endColumn": 34, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 84, + "column": 22, + "endLine": 84, + "endColumn": 34, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 87, + "column": 34, + "endLine": 87, + "endColumn": 46, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 93, + "column": 10, + "endLine": 93, + "endColumn": 22, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 98, + "column": 10, + "endLine": 98, + "endColumn": 22, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 107, + "column": 6, + "endLine": 107, + "endColumn": 19, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 113, + "column": 6, + "endLine": 113, + "endColumn": 19, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 119, + "column": 6, + "endLine": 119, + "endColumn": 18, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + }, + { + "line": 123, + "column": 6, + "endLine": 123, + "endColumn": 19, + "problem": "ObjectLiteralUnionNeedsCast", + "suggest": "", + "rule": "Object literal used with a union type. The intended union member (e.g. { … } as A) must be explicitly asserted (arkts-union-assignment-with-obj-literal-ambiguity)", + "severity": "ERROR" + } + ] +} diff --git a/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets.json b/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets.json new file mode 100644 index 0000000000000000000000000000000000000000..63a94f9a39b7a47afba1936a8dc4e7986ef99b6b --- /dev/null +++ b/ets2panda/linter/test/main/union_assignment_with_obj_literal_ambiguity.ets.json @@ -0,0 +1,38 @@ +{ + "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": 53, + "column": 18, + "endLine": 53, + "endColumn": 21, + "problem": "AnyType", + "suggest": "", + "rule": "Use explicit types instead of \"any\", \"unknown\" (arkts-no-any-unknown)", + "severity": "ERROR" + }, + { + "line": 53, + "column": 36, + "endLine": 53, + "endColumn": 37, + "problem": "ObjectLiteralNoContextType", + "suggest": "", + "rule": "Object literal must correspond to some explicitly declared class or interface (arkts-no-untyped-obj-literals)", + "severity": "ERROR" + } + ] +}