From e5095ee45d28a77eb0ac3d61ef4ab0c9506f3b8a Mon Sep 17 00:00:00 2001 From: Konstantin Baladurin Date: Wed, 23 Aug 2023 16:49:26 +0300 Subject: [PATCH] Linter: relax initialization rules in case of dynamic types Signed-off-by: Konstantin Baladurin --- linter-4.2/src/TypeScriptLinter.ts | 10 +- linter-4.2/src/Utils.ts | 91 +++++++++++++++++++ linter-4.2/test/dynamic_lib.d.ts | 7 ++ linter-4.2/test/dynamic_object_literals.ts | 35 +++++++ .../dynamic_object_literals.ts.autofix.skip | 0 .../dynamic_object_literals.ts.relax.json | 17 ++++ .../dynamic_object_literals.ts.strict.json | 17 ++++ linter/src/TypeScriptLinter.ts | 11 ++- linter/src/utils/TsUtils.ts | 90 ++++++++++++++++++ linter/test/dynamic_lib.d.ts | 7 ++ linter/test/dynamic_object_literals.ts | 35 +++++++ .../dynamic_object_literals.ts.autofix.skip | 0 .../dynamic_object_literals.ts.relax.json | 17 ++++ .../dynamic_object_literals.ts.strict.json | 17 ++++ 14 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 linter-4.2/test/dynamic_lib.d.ts create mode 100644 linter-4.2/test/dynamic_object_literals.ts create mode 100644 linter-4.2/test/dynamic_object_literals.ts.autofix.skip create mode 100644 linter-4.2/test/dynamic_object_literals.ts.relax.json create mode 100644 linter-4.2/test/dynamic_object_literals.ts.strict.json create mode 100644 linter/test/dynamic_lib.d.ts create mode 100644 linter/test/dynamic_object_literals.ts create mode 100644 linter/test/dynamic_object_literals.ts.autofix.skip create mode 100644 linter/test/dynamic_object_literals.ts.relax.json create mode 100644 linter/test/dynamic_object_literals.ts.strict.json diff --git a/linter-4.2/src/TypeScriptLinter.ts b/linter-4.2/src/TypeScriptLinter.ts index dafd72b8f..1a44a8e9a 100644 --- a/linter-4.2/src/TypeScriptLinter.ts +++ b/linter-4.2/src/TypeScriptLinter.ts @@ -451,6 +451,7 @@ export class TypeScriptLinter { let objectLiteralType = this.tsTypeChecker.getContextualType(objectLiteralExpr); if (!this.tsUtils.isStructObjectInitializer(objectLiteralExpr) && + !this.tsUtils.isDynamicLiteralInitializer(objectLiteralExpr) && !this.tsUtils.areTypesAssignable(objectLiteralType, objectLiteralExpr)) this.incrementCounters(node, FaultID.ObjectLiteralNoContextType); } @@ -473,7 +474,8 @@ export class TypeScriptLinter { for(let element of arrayLitElements ) { if(ts.isObjectLiteralExpression(element)) { let objectLiteralType = this.tsTypeChecker.getContextualType(element); - if (!this.tsUtils.areTypesAssignable(objectLiteralType, element)) { + if (!this.tsUtils.isDynamicLiteralInitializer(arrayLitNode) && + !this.tsUtils.areTypesAssignable(objectLiteralType, element)) { noContextTypeForArrayLiteral = true; break; } @@ -645,14 +647,16 @@ export class TypeScriptLinter { let propName = (node as ts.PropertyAssignment | ts.PropertyDeclaration).name; if (propName && (propName.kind === ts.SyntaxKind.NumericLiteral || propName.kind === ts.SyntaxKind.StringLiteral)) { - // We can use literals as property names only when creating Record instances. + // We can use literals as property names only when creating Record or any interop instances. let isRecordObjectInitializer = false; + let isDynamicLiteralInitializer = false; if (ts.isPropertyAssignment(node)) { let objectLiteralType = this.tsTypeChecker.getContextualType(node.parent); isRecordObjectInitializer = !!objectLiteralType && this.tsUtils.isStdRecordType(objectLiteralType); + isDynamicLiteralInitializer = this.tsUtils.isDynamicLiteralInitializer(node.parent); } - if (!isRecordObjectInitializer) { + if (!isRecordObjectInitializer && !isDynamicLiteralInitializer) { let autofix : Autofix[] | undefined = Autofixer.fixLiteralAsPropertyName(node); let autofixable = autofix != undefined; if (!this.autofixesInfo.shouldAutofix(node, FaultID.LiteralAsPropertyName)) { diff --git a/linter-4.2/src/Utils.ts b/linter-4.2/src/Utils.ts index e353dce55..6e9acc51c 100644 --- a/linter-4.2/src/Utils.ts +++ b/linter-4.2/src/Utils.ts @@ -1005,4 +1005,95 @@ export class TsUtils { return ts.ScriptKind.Unknown; } } + + public isStdLibraryType(type: ts.Type): boolean { + return this.isStdLibrarySymbol(type.aliasSymbol ?? type.getSymbol()); + } + + public isStdLibrarySymbol(sym: ts.Symbol | undefined) { + if (sym && sym.declarations && sym.declarations.length > 0) { + const srcFile = sym.declarations[0].getSourceFile(); + return srcFile && + TsUtils.STANDARD_LIBRARIES.includes(path.basename(srcFile.fileName).toLowerCase()); + } + + return false; + } + + public isDynamicType(type: ts.Type | undefined): boolean | undefined { + if (type === undefined) { + return false; + } + + // Return 'true' if it is an object of library type initialization, otherwise + // return 'false' if it is not an object of standard library type one. + // In the case of standard library type we need to determine context. + + if (type.isUnion()) { + for (let compType of type.types) { + if (this.isLibraryType(compType)) { + return true; + } + + if (!this.isStdLibraryType(compType)) { + return false; + } + } + } + + if (this.isLibraryType(type)) { + return true; + } + + if (!this.isStdLibraryType(type)) { + return false; + } + + return undefined; + } + + public isDynamicLiteralInitializer(expr: ts.Expression): boolean { + if (!ts.isObjectLiteralExpression(expr) && !ts.isArrayLiteralExpression(expr)) { + return false; + } + + // Handle nested literals: + // { f: { ... } } + let curNode: ts.Node = expr; + while (ts.isObjectLiteralExpression(curNode) || ts.isArrayLiteralExpression(curNode)) { + const exprType = this.tsTypeChecker.getContextualType(curNode); + if (exprType !== undefined) { + const res = this.isDynamicType(exprType) + if (res !== undefined) { + return res; + } + } + + curNode = curNode.parent; + if (ts.isPropertyAssignment(curNode)) { + curNode = curNode.parent; + } + } + + // Handle calls with literals: + // foo({ ... }) + if (ts.isCallExpression(curNode)) { + const callExpr = curNode as ts.CallExpression; + const sym = this.tsTypeChecker.getTypeAtLocation(callExpr.expression).symbol; + return this.isLibrarySymbol(sym); + } + + // Handle property assignments with literals: + // obj.f = { ... } + if (ts.isBinaryExpression(curNode)) { + const binExpr = curNode as ts.BinaryExpression; + if (ts.isPropertyAccessExpression(binExpr.left)) { + const propAccessExpr = binExpr.left as ts.PropertyAccessExpression; + const type = this.tsTypeChecker.getTypeAtLocation(propAccessExpr.expression); + return this.isLibrarySymbol(type.symbol); + } + } + + return false; + } } diff --git a/linter-4.2/test/dynamic_lib.d.ts b/linter-4.2/test/dynamic_lib.d.ts new file mode 100644 index 000000000..9bc145621 --- /dev/null +++ b/linter-4.2/test/dynamic_lib.d.ts @@ -0,0 +1,7 @@ +export declare interface I { + f1: Object; + f2: Array; + [key: string]: Object; +} + +export declare function foo(p: Object): void \ No newline at end of file diff --git a/linter-4.2/test/dynamic_object_literals.ts b/linter-4.2/test/dynamic_object_literals.ts new file mode 100644 index 000000000..562860699 --- /dev/null +++ b/linter-4.2/test/dynamic_object_literals.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2023 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. + */ + +import { I, foo } from "./dynamic_lib" + +function main(): void { + let obj: I = { + f1: {a: 10, b: 20}, + f2: [{c: 30}, {d: 40}], + "prop.xxx": 0 + } + + obj = { + f1: {e: "abc"}, + f2: [{g: 50}] + } + + obj.f1 = {f: 100} + obj.f1 = [{a1: 1}, {a2: 2, a3: 3}] + + foo({f2: 'abc', f3: 30}) + foo([{b1: 1, b2: 2}, {b3: '3'}]) +} \ No newline at end of file diff --git a/linter-4.2/test/dynamic_object_literals.ts.autofix.skip b/linter-4.2/test/dynamic_object_literals.ts.autofix.skip new file mode 100644 index 000000000..e69de29bb diff --git a/linter-4.2/test/dynamic_object_literals.ts.relax.json b/linter-4.2/test/dynamic_object_literals.ts.relax.json new file mode 100644 index 000000000..e7d2d6779 --- /dev/null +++ b/linter-4.2/test/dynamic_object_literals.ts.relax.json @@ -0,0 +1,17 @@ +{ + "copyright": [ + "Copyright (c) 2023-2023 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." + ], + "nodes": [] +} \ No newline at end of file diff --git a/linter-4.2/test/dynamic_object_literals.ts.strict.json b/linter-4.2/test/dynamic_object_literals.ts.strict.json new file mode 100644 index 000000000..e7d2d6779 --- /dev/null +++ b/linter-4.2/test/dynamic_object_literals.ts.strict.json @@ -0,0 +1,17 @@ +{ + "copyright": [ + "Copyright (c) 2023-2023 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." + ], + "nodes": [] +} \ No newline at end of file diff --git a/linter/src/TypeScriptLinter.ts b/linter/src/TypeScriptLinter.ts index 0c9ef14d4..62714dac3 100644 --- a/linter/src/TypeScriptLinter.ts +++ b/linter/src/TypeScriptLinter.ts @@ -423,6 +423,7 @@ export class TypeScriptLinter { // issue 13082: Allow initializing struct instances with object literal. let objectLiteralType = this.tsTypeChecker.getContextualType(objectLiteralExpr); if (!this.tsUtils.isStructObjectInitializer(objectLiteralExpr) && + !this.tsUtils.isDynamicLiteralInitializer(objectLiteralExpr) && !this.tsUtils.areTypesAssignable(objectLiteralType, objectLiteralExpr)) { this.incrementCounters(node, FaultID.ObjectLiteralNoContextType); } @@ -447,7 +448,8 @@ export class TypeScriptLinter { for(let element of arrayLitElements ) { if(ts.isObjectLiteralExpression(element)) { let objectLiteralType = this.tsTypeChecker.getContextualType(element); - if (!this.tsUtils.areTypesAssignable(objectLiteralType, element)) { + if (!this.tsUtils.isDynamicLiteralInitializer(arrayLitNode) && + !this.tsUtils.areTypesAssignable(objectLiteralType, element)) { noContextTypeForArrayLiteral = true; break; } @@ -627,13 +629,16 @@ export class TypeScriptLinter { private handlePropertyAssignmentOrDeclaration(node: ts.Node) { let propName = (node as ts.PropertyAssignment | ts.PropertyDeclaration).name; if (propName && (propName.kind === ts.SyntaxKind.NumericLiteral || propName.kind === ts.SyntaxKind.StringLiteral)) { - // We can use literals as property names only when creating Record instances. + // We can use literals as property names only when creating Record or any interop instances. let isRecordObjectInitializer = false; + let isDynamicLiteralInitializer = false; if (ts.isPropertyAssignment(node)) { let objectLiteralType = this.tsTypeChecker.getContextualType(node.parent); isRecordObjectInitializer = !!objectLiteralType && this.tsUtils.isStdRecordType(objectLiteralType); + isDynamicLiteralInitializer = this.tsUtils.isDynamicLiteralInitializer(node.parent); } - if (!isRecordObjectInitializer) { + + if (!isRecordObjectInitializer && !isDynamicLiteralInitializer) { let autofix : Autofix[] | undefined = Autofixer.fixLiteralAsPropertyName(node); let autofixable = autofix != undefined; if (!this.autofixesInfo.shouldAutofix(node, FaultID.LiteralAsPropertyName)) { diff --git a/linter/src/utils/TsUtils.ts b/linter/src/utils/TsUtils.ts index 45a096714..57c0837f5 100644 --- a/linter/src/utils/TsUtils.ts +++ b/linter/src/utils/TsUtils.ts @@ -843,4 +843,94 @@ export class TsUtils { const sym = type.getSymbol(); return sym && sym.getName() === 'Function' && this.isGlobalSymbol(sym); } + + public isStdLibraryType(type: ts.Type): boolean { + return this.isStdLibrarySymbol(type.aliasSymbol ?? type.getSymbol()); + } + + public isStdLibrarySymbol(sym: ts.Symbol | undefined) { + if (sym && sym.declarations && sym.declarations.length > 0) { + const srcFile = sym.declarations[0].getSourceFile(); + return srcFile && STANDARD_LIBRARIES.includes(path.basename(srcFile.fileName).toLowerCase()); + } + + return false; + } + + public isDynamicType(type: ts.Type | undefined): boolean | undefined { + if (type === undefined) { + return false; + } + + // Return 'true' if it is an object of library type initialization, otherwise + // return 'false' if it is not an object of standard library type one. + // In the case of standard library type we need to determine context. + + if (type.isUnion()) { + for (let compType of type.types) { + if (this.isLibraryType(compType)) { + return true; + } + + if (!this.isStdLibraryType(compType)) { + return false; + } + } + } + + if (this.isLibraryType(type)) { + return true; + } + + if (!this.isStdLibraryType(type)) { + return false; + } + + return undefined; + } + + public isDynamicLiteralInitializer(expr: ts.Expression): boolean { + if (!ts.isObjectLiteralExpression(expr) && !ts.isArrayLiteralExpression(expr)) { + return false; + } + + // Handle nested literals: + // { f: { ... } } + let curNode: ts.Node = expr; + while (ts.isObjectLiteralExpression(curNode) || ts.isArrayLiteralExpression(curNode)) { + const exprType = this.tsTypeChecker.getContextualType(curNode); + if (exprType !== undefined) { + const res = this.isDynamicType(exprType) + if (res !== undefined) { + return res; + } + } + + curNode = curNode.parent; + if (ts.isPropertyAssignment(curNode)) { + curNode = curNode.parent; + } + } + + // Handle calls with literals: + // foo({ ... }) + if (ts.isCallExpression(curNode)) { + const callExpr = curNode as ts.CallExpression; + const sym = this.tsTypeChecker.getTypeAtLocation(callExpr.expression).symbol; + return this.isLibrarySymbol(sym); + } + + // Handle property assignments with literals: + // obj.f = { ... } + if (ts.isBinaryExpression(curNode)) { + const binExpr = curNode as ts.BinaryExpression; + if (ts.isPropertyAccessExpression(binExpr.left)) { + const propAccessExpr = binExpr.left as ts.PropertyAccessExpression; + const type = this.tsTypeChecker.getTypeAtLocation(propAccessExpr.expression); + return this.isLibrarySymbol(type.symbol); + } + } + + return false; + } } diff --git a/linter/test/dynamic_lib.d.ts b/linter/test/dynamic_lib.d.ts new file mode 100644 index 000000000..9bc145621 --- /dev/null +++ b/linter/test/dynamic_lib.d.ts @@ -0,0 +1,7 @@ +export declare interface I { + f1: Object; + f2: Array; + [key: string]: Object; +} + +export declare function foo(p: Object): void \ No newline at end of file diff --git a/linter/test/dynamic_object_literals.ts b/linter/test/dynamic_object_literals.ts new file mode 100644 index 000000000..562860699 --- /dev/null +++ b/linter/test/dynamic_object_literals.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2023 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. + */ + +import { I, foo } from "./dynamic_lib" + +function main(): void { + let obj: I = { + f1: {a: 10, b: 20}, + f2: [{c: 30}, {d: 40}], + "prop.xxx": 0 + } + + obj = { + f1: {e: "abc"}, + f2: [{g: 50}] + } + + obj.f1 = {f: 100} + obj.f1 = [{a1: 1}, {a2: 2, a3: 3}] + + foo({f2: 'abc', f3: 30}) + foo([{b1: 1, b2: 2}, {b3: '3'}]) +} \ No newline at end of file diff --git a/linter/test/dynamic_object_literals.ts.autofix.skip b/linter/test/dynamic_object_literals.ts.autofix.skip new file mode 100644 index 000000000..e69de29bb diff --git a/linter/test/dynamic_object_literals.ts.relax.json b/linter/test/dynamic_object_literals.ts.relax.json new file mode 100644 index 000000000..e7d2d6779 --- /dev/null +++ b/linter/test/dynamic_object_literals.ts.relax.json @@ -0,0 +1,17 @@ +{ + "copyright": [ + "Copyright (c) 2023-2023 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." + ], + "nodes": [] +} \ No newline at end of file diff --git a/linter/test/dynamic_object_literals.ts.strict.json b/linter/test/dynamic_object_literals.ts.strict.json new file mode 100644 index 000000000..e7d2d6779 --- /dev/null +++ b/linter/test/dynamic_object_literals.ts.strict.json @@ -0,0 +1,17 @@ +{ + "copyright": [ + "Copyright (c) 2023-2023 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." + ], + "nodes": [] +} \ No newline at end of file -- Gitee