From 05cfde7c6eb53c373ea0d6ae1e793cc2dc639dc3 Mon Sep 17 00:00:00 2001 From: Max Sysoev Date: Mon, 12 Aug 2019 16:43:30 +0300 Subject: [PATCH] Add rule: strict-string-expressions (#4807) * feat: add strict-string-expressions * docs(strictStringExpressionsRule): add option desc * feat(strictStringExpressionsRule): add fixer * test: add strict-string-expressions into all.ts * type: remove declare * refactor(strictStringExpressionsRule): eliminate copy-paste * style: fix tslint errors * fix(strictStringExpressionsRule): exclude any type from checks * style: fix sytle erorrs * refactor: rename function name to more cohesive * refactor(strictStringExpressionsRule): add more renaimings * style(strictStringExpressionsRule): prettify * fix: do not require numbers to be stringified * test(strictStringExpressions): add string and number literals * test(strictStringExpressionsRule): fix nit * test(strictStringExpressionsRule): add string uni in test * test: add typeof window case * feat: handle union type * feat(strictStringExpressionsRule): add allow-empty-types option * feat: allo empty types in all.ts config * style(strictStringExpressionsRule): fix lint rules * feat(strictStringExpressionsRule): allow booleans * style(linter): fix codestyle * refactor(strictStringExpressionsRule): rename private helpers * docs(strictStringExpressionsRule): update license year * refactor(strictStringExpressionsRule): make allow-empty-types to appear default option * test(stringStrictExpressions): correct test --- src/configs/all.ts | 1 + src/linter.ts | 2 +- src/rules/strictStringExpressionsRule.ts | 159 ++++++++++++++++++ .../allow-empty-types/test.ts.fix | 133 +++++++++++++++ .../allow-empty-types/test.ts.lint | 145 ++++++++++++++++ .../allow-empty-types/tsconfig.json | 4 + .../allow-empty-types/tslint.json | 10 ++ .../disallow-empty-types/test.ts.fix | 133 +++++++++++++++ .../disallow-empty-types/test.ts.lint | 151 +++++++++++++++++ .../disallow-empty-types/tsconfig.json | 5 + .../disallow-empty-types/tslint.json | 10 ++ 11 files changed, 752 insertions(+), 1 deletion(-) create mode 100644 src/rules/strictStringExpressionsRule.ts create mode 100644 test/rules/strict-string-expressions/allow-empty-types/test.ts.fix create mode 100644 test/rules/strict-string-expressions/allow-empty-types/test.ts.lint create mode 100644 test/rules/strict-string-expressions/allow-empty-types/tsconfig.json create mode 100644 test/rules/strict-string-expressions/allow-empty-types/tslint.json create mode 100644 test/rules/strict-string-expressions/disallow-empty-types/test.ts.fix create mode 100644 test/rules/strict-string-expressions/disallow-empty-types/test.ts.lint create mode 100644 test/rules/strict-string-expressions/disallow-empty-types/tsconfig.json create mode 100644 test/rules/strict-string-expressions/disallow-empty-types/tslint.json diff --git a/src/configs/all.ts b/src/configs/all.ts index 4bf818be218..cdbf33dc59b 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -151,6 +151,7 @@ export const rules = { "restrict-plus-operands": true, "static-this": true, "strict-boolean-expressions": true, + "strict-string-expressions": true, "strict-comparisons": true, "strict-type-predicates": true, "switch-default": true, diff --git a/src/linter.ts b/src/linter.ts index 47edb42312a..5a86ae92772 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -187,7 +187,7 @@ export class Linter { this.options.formatter !== undefined ? this.options.formatter : "prose"; const Formatter = findFormatter(formatterName, this.options.formattersDirectory); if (Formatter === undefined) { - throw new Error(`formatter '${formatterName}' not found`); + throw new Error(`formatter '${String(formatterName)}' not found`); } const formatter = new Formatter(); diff --git a/src/rules/strictStringExpressionsRule.ts b/src/rules/strictStringExpressionsRule.ts new file mode 100644 index 00000000000..e614a77f9ae --- /dev/null +++ b/src/rules/strictStringExpressionsRule.ts @@ -0,0 +1,159 @@ +/** + * @license + * Copyright 2019 Palantir Technologies, Inc. + * + * 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 { isTypeFlagSet, isUnionType } from "tsutils"; +import * as ts from "typescript"; + +import * as Lint from "../index"; + +const OPTION_ALLOW_EMPTY_TYPES = "allow-empty-types"; + +interface Options { + [OPTION_ALLOW_EMPTY_TYPES]?: boolean; +} + +export class Rule extends Lint.Rules.TypedRule { + public static CONVERSION_REQUIRED = "Explicit conversion to string type required"; + + public static metadata: Lint.IRuleMetadata = { + description: "Disable implicit toString() calls", + descriptionDetails: Lint.Utils.dedent` + Require explicit toString() call for variables used in strings. By default only strings are allowed. + + The following nodes are checked: + + * String literals ("foo" + bar) + * ES6 templates (\`foo \${bar}\`)`, + hasFix: true, + optionExamples: [ + true, + [ + true, + { + [OPTION_ALLOW_EMPTY_TYPES]: true, + }, + ], + ], + options: { + properties: { + [OPTION_ALLOW_EMPTY_TYPES]: { + type: "boolean", + }, + }, + type: "object", + }, + optionsDescription: Lint.Utils.dedent` + Following arguments may be optionally provided: + * \`${OPTION_ALLOW_EMPTY_TYPES}\` allows \`null\`, \`undefined\` and \`never\` to be passed into strings without explicit conversion`, + requiresTypeInfo: true, + ruleName: "strict-string-expressions", + type: "functionality", + typescriptOnly: true, + }; + + public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + return this.applyWithFunction( + sourceFile, + walk, + this.getRuleOptions(), + program.getTypeChecker(), + ); + } + + private getRuleOptions(): Options { + if (this.ruleArguments[0] === undefined) { + return { + [OPTION_ALLOW_EMPTY_TYPES]: true, + }; + } else { + return this.ruleArguments[0] as Options; + } + } +} + +function walk(ctx: Lint.WalkContext, checker: ts.TypeChecker): void { + const { sourceFile, options } = ctx; + ts.forEachChild(sourceFile, function cb(node: ts.Node): void { + switch (node.kind) { + case ts.SyntaxKind.BinaryExpression: { + const binaryExpr = node as ts.BinaryExpression; + if (binaryExpr.operatorToken.kind === ts.SyntaxKind.PlusToken) { + const leftIsPassedAsIs = typeCanBeStringifiedEasily( + checker.getTypeAtLocation(binaryExpr.left), + options, + ); + const rightIsPassedAsIs = typeCanBeStringifiedEasily( + checker.getTypeAtLocation(binaryExpr.right), + options, + ); + const leftIsFailed = !leftIsPassedAsIs && rightIsPassedAsIs; + const rightIsFailed = leftIsPassedAsIs && !rightIsPassedAsIs; + if (leftIsFailed || rightIsFailed) { + const expression = leftIsFailed ? binaryExpr.left : binaryExpr.right; + addFailure(binaryExpr, expression); + } + } + break; + } + case ts.SyntaxKind.TemplateSpan: { + const templateSpanNode = node as ts.TemplateSpan; + const type = checker.getTypeAtLocation(templateSpanNode.expression); + const shouldPassAsIs = typeCanBeStringifiedEasily(type, options); + if (!shouldPassAsIs) { + const { expression } = templateSpanNode; + addFailure(templateSpanNode, expression); + } + } + } + return ts.forEachChild(node, cb); + }); + + function addFailure(node: ts.Node, expression: ts.Expression) { + const fix = Lint.Replacement.replaceFromTo( + expression.getStart(), + expression.end, + `String(${expression.getText()})`, + ); + ctx.addFailureAtNode(node, Rule.CONVERSION_REQUIRED, fix); + } +} + +const typeIsEmpty = (type: ts.Type): boolean => + isTypeFlagSet(type, ts.TypeFlags.Null) || + isTypeFlagSet(type, ts.TypeFlags.VoidLike) || + isTypeFlagSet(type, ts.TypeFlags.Undefined) || + isTypeFlagSet(type, ts.TypeFlags.Never); + +function typeCanBeStringifiedEasily(type: ts.Type, options: Options): boolean { + if (isUnionType(type)) { + return type.types.every(unionAtomicType => + typeCanBeStringifiedEasily(unionAtomicType, options), + ); + } + + if (options[OPTION_ALLOW_EMPTY_TYPES] && typeIsEmpty(type)) { + return true; + } + + return ( + isTypeFlagSet(type, ts.TypeFlags.BooleanLike) || + isTypeFlagSet(type, ts.TypeFlags.StringOrNumberLiteral) || + isTypeFlagSet(type, ts.TypeFlags.NumberLike) || + isTypeFlagSet(type, ts.TypeFlags.StringLike) || + isTypeFlagSet(type, ts.TypeFlags.Any) + ); +} diff --git a/test/rules/strict-string-expressions/allow-empty-types/test.ts.fix b/test/rules/strict-string-expressions/allow-empty-types/test.ts.fix new file mode 100644 index 00000000000..ed0c9eec177 --- /dev/null +++ b/test/rules/strict-string-expressions/allow-empty-types/test.ts.fix @@ -0,0 +1,133 @@ + const fooAny: any; + const fooStr: string = 'foo'; + const fooNumber = 2; + class FooClass {} + class ClassWithToString { + public static toString () { return ''; } + public toString () { return ''; } + } + const classWithToString = new ClassWithToString(); + const FooStr = new String('foo'); + const fooArr = ['foo']; + const emptyArr = []; + const stringUni = "foo" | "bar"; + const booleanVar: boolean; + + `foo` + `${'str literal'}` + `${123}` + `${booleanVar}` + `${fooAny}` + `${fooStr}` + `${stringUni}` + `${fooNumber}` + `${(typeof window)}` + `${String(FooClass)}` + `${String(ClassWithToString)}` + `${String(classWithToString)}` + `${String(FooStr)}` + `${String(fooArr)}` + `${String(emptyArr)}` + + `${String('str literal')}` + `${String(123)}` + `${String(booleanVar)}` + `${String(fooAny)}` + `${String(fooStr)}` + `${String(stringUni)}` + `${String(fooNumber)}` + `${String((typeof window))}` + `${String(FooClass)}` + `${String(ClassWithToString)}` + `${String(classWithToString)}` + `${String(FooStr)}` + `${String(fooArr)}` + `${String(emptyArr)}` + + `${'str literal'.toString()}` + `${123..toString()}` + `${booleanVar.toString()}` + `${fooAny.toString()}` + `${fooStr.toString()}` + `${stringUni.toString()}` + `${fooNumber.toString()}` + `${(typeof window).toString()}` + `${FooClass.toString()}` + `${ClassWithToString.toString()}` + `${classWithToString.toString()}` + `${FooStr.toString()}` + `${fooArr.toString()}` + `${emptyArr.toString()}` + + 'str' + 'str literal' + 'str' + 'str' + 123 + 'str' + 'str' + booleanVar + 'str' + 'str' + fooAny + 'str' + 'str' + fooStr + 'str' + 'str' + stringUni + 'str' + 'str' + fooNumber + 'str' + 'str' + (typeof window) + 'str' + 'str' + String(FooClass) + 'str' + 'str' + String(ClassWithToString) + 'str' + 'str' + String(classWithToString) + 'str' + 'str' + String(FooStr) + 'str' + 'str' + String(fooArr) + 'str' + 'str' + String(emptyArr) + 'str' + + 'str' + String('str literal') + 'str' + 'str' + String(123) + 'str' + 'str' + String(booleanVar) + 'str' + 'str' + String(fooAny) + 'str' + 'str' + String(fooStr) + 'str' + 'str' + String(stringUni) + 'str' + 'str' + String(fooNumber) + 'str' + 'str' + String((typeof window)) + 'str' + 'str' + String(FooClass) + 'str' + 'str' + String(ClassWithToString) + 'str' + 'str' + String(classWithToString) + 'str' + 'str' + String(FooStr) + 'str' + 'str' + String(fooArr) + 'str' + 'str' + String(emptyArr) + 'str' + + 'str' + 'str literal'.toString() + 'str' + 'str' + 123..toString() + 'str' + 'str' + booleanVar.toString() + 'str' + 'str' + fooAny.toString() + 'str' + 'str' + fooStr.toString() + 'str' + 'str' + stringUni.toString() + 'str' + 'str' + fooNumber.toString() + 'str' + 'str' + (typeof window).toString() + 'str' + 'str' + FooClass.toString() + 'str' + 'str' + ClassWithToString.toString() + 'str' + 'str' + classWithToString.toString() + 'str' + 'str' + FooStr.toString() + 'str' + 'str' + fooArr.toString() + 'str' + 'str' + emptyArr.toString() + 'str' + + const barFooStrOrUndef: string | undefined; + const barFooStrOrNull: string | null; + const neverType: never; + + `${barFooStrOrUndef}` + `${barFooStrOrNull}` + `${neverType}` + + `${String(barFooStrOrUndef)}` + `${String(barFooStrOrNull)}` + `${String(neverType)}` + + `${barFooStrOrUndef.toString()}` + `${barFooStrOrNull.toString()}` + `${neverType.toString()}` + + 'str' + barFooStrOrUndef + 'str' + 'str' + barFooStrOrNull + 'str' + 'str' + neverType + 'str' + + 'str' + String(barFooStrOrUndef) + 'str' + 'str' + String(barFooStrOrNull) + 'str' + 'str' + String(neverType) + 'str' + + 'str' + barFooStrOrUndef.toString() + 'str' + 'str' + barFooStrOrNull.toString() + 'str' + 'str' + neverType.toString() + 'str' diff --git a/test/rules/strict-string-expressions/allow-empty-types/test.ts.lint b/test/rules/strict-string-expressions/allow-empty-types/test.ts.lint new file mode 100644 index 00000000000..0660ca94379 --- /dev/null +++ b/test/rules/strict-string-expressions/allow-empty-types/test.ts.lint @@ -0,0 +1,145 @@ + const fooAny: any; + const fooStr: string = 'foo'; + const fooNumber = 2; + class FooClass {} + class ClassWithToString { + public static toString () { return ''; } + public toString () { return ''; } + } + const classWithToString = new ClassWithToString(); + const FooStr = new String('foo'); + const fooArr = ['foo']; + const emptyArr = []; + const stringUni = "foo" | "bar"; + const booleanVar: boolean; + + `foo` + `${'str literal'}` + `${123}` + `${booleanVar}` + `${fooAny}` + `${fooStr}` + `${stringUni}` + `${fooNumber}` + `${(typeof window)}` + `${FooClass}` + ~~~~~~~~~~ [Explicit conversion to string type required] + `${ClassWithToString}` + ~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + `${classWithToString}` + ~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + `${FooStr}` + ~~~~~~~~ [Explicit conversion to string type required] + `${fooArr}` + ~~~~~~~~ [Explicit conversion to string type required] + `${emptyArr}` + ~~~~~~~~~~ [Explicit conversion to string type required] + + `${String('str literal')}` + `${String(123)}` + `${String(booleanVar)}` + `${String(fooAny)}` + `${String(fooStr)}` + `${String(stringUni)}` + `${String(fooNumber)}` + `${String((typeof window))}` + `${String(FooClass)}` + `${String(ClassWithToString)}` + `${String(classWithToString)}` + `${String(FooStr)}` + `${String(fooArr)}` + `${String(emptyArr)}` + + `${'str literal'.toString()}` + `${123..toString()}` + `${booleanVar.toString()}` + `${fooAny.toString()}` + `${fooStr.toString()}` + `${stringUni.toString()}` + `${fooNumber.toString()}` + `${(typeof window).toString()}` + `${FooClass.toString()}` + `${ClassWithToString.toString()}` + `${classWithToString.toString()}` + `${FooStr.toString()}` + `${fooArr.toString()}` + `${emptyArr.toString()}` + + 'str' + 'str literal' + 'str' + 'str' + 123 + 'str' + 'str' + booleanVar + 'str' + 'str' + fooAny + 'str' + 'str' + fooStr + 'str' + 'str' + stringUni + 'str' + 'str' + fooNumber + 'str' + 'str' + (typeof window) + 'str' + 'str' + FooClass + 'str' + ~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + ClassWithToString + 'str' + ~~~~~~~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + classWithToString + 'str' + ~~~~~~~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + FooStr + 'str' + ~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + fooArr + 'str' + ~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + emptyArr + 'str' + ~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + + 'str' + String('str literal') + 'str' + 'str' + String(123) + 'str' + 'str' + String(booleanVar) + 'str' + 'str' + String(fooAny) + 'str' + 'str' + String(fooStr) + 'str' + 'str' + String(stringUni) + 'str' + 'str' + String(fooNumber) + 'str' + 'str' + String((typeof window)) + 'str' + 'str' + String(FooClass) + 'str' + 'str' + String(ClassWithToString) + 'str' + 'str' + String(classWithToString) + 'str' + 'str' + String(FooStr) + 'str' + 'str' + String(fooArr) + 'str' + 'str' + String(emptyArr) + 'str' + + 'str' + 'str literal'.toString() + 'str' + 'str' + 123..toString() + 'str' + 'str' + booleanVar.toString() + 'str' + 'str' + fooAny.toString() + 'str' + 'str' + fooStr.toString() + 'str' + 'str' + stringUni.toString() + 'str' + 'str' + fooNumber.toString() + 'str' + 'str' + (typeof window).toString() + 'str' + 'str' + FooClass.toString() + 'str' + 'str' + ClassWithToString.toString() + 'str' + 'str' + classWithToString.toString() + 'str' + 'str' + FooStr.toString() + 'str' + 'str' + fooArr.toString() + 'str' + 'str' + emptyArr.toString() + 'str' + + const barFooStrOrUndef: string | undefined; + const barFooStrOrNull: string | null; + const neverType: never; + + `${barFooStrOrUndef}` + `${barFooStrOrNull}` + `${neverType}` + + `${String(barFooStrOrUndef)}` + `${String(barFooStrOrNull)}` + `${String(neverType)}` + + `${barFooStrOrUndef.toString()}` + `${barFooStrOrNull.toString()}` + `${neverType.toString()}` + + 'str' + barFooStrOrUndef + 'str' + 'str' + barFooStrOrNull + 'str' + 'str' + neverType + 'str' + + 'str' + String(barFooStrOrUndef) + 'str' + 'str' + String(barFooStrOrNull) + 'str' + 'str' + String(neverType) + 'str' + + 'str' + barFooStrOrUndef.toString() + 'str' + 'str' + barFooStrOrNull.toString() + 'str' + 'str' + neverType.toString() + 'str' diff --git a/test/rules/strict-string-expressions/allow-empty-types/tsconfig.json b/test/rules/strict-string-expressions/allow-empty-types/tsconfig.json new file mode 100644 index 00000000000..aa5798f3ce6 --- /dev/null +++ b/test/rules/strict-string-expressions/allow-empty-types/tsconfig.json @@ -0,0 +1,4 @@ +{ + "compilerOptions": { + } +} diff --git a/test/rules/strict-string-expressions/allow-empty-types/tslint.json b/test/rules/strict-string-expressions/allow-empty-types/tslint.json new file mode 100644 index 00000000000..270026fc7f4 --- /dev/null +++ b/test/rules/strict-string-expressions/allow-empty-types/tslint.json @@ -0,0 +1,10 @@ +{ + "rules": { + "strict-string-expressions": [ + true, + { + "allow-empty-types": true + } + ] + } +} diff --git a/test/rules/strict-string-expressions/disallow-empty-types/test.ts.fix b/test/rules/strict-string-expressions/disallow-empty-types/test.ts.fix new file mode 100644 index 00000000000..dd4b6ee8d2c --- /dev/null +++ b/test/rules/strict-string-expressions/disallow-empty-types/test.ts.fix @@ -0,0 +1,133 @@ + const fooAny: any; + const fooStr: string = 'foo'; + const fooNumber = 2; + class FooClass {} + class ClassWithToString { + public static toString () { return ''; } + public toString () { return ''; } + } + const classWithToString = new ClassWithToString(); + const FooStr = new String('foo'); + const fooArr = ['foo']; + const emptyArr = []; + const stringUni = "foo" | "bar"; + const booleanVar: boolean; + + `foo` + `${'str literal'}` + `${123}` + `${fooAny}` + `${fooStr}` + `${stringUni}` + `${fooNumber}` + `${(typeof window)}` + `${String(FooClass)}` + `${String(ClassWithToString)}` + `${String(classWithToString)}` + `${String(FooStr)}` + `${String(fooArr)}` + `${String(emptyArr)}` + `${booleanVar}` + + `${String('str literal')}` + `${String(123)}` + `${String(fooAny)}` + `${String(fooStr)}` + `${String(stringUni)}` + `${String(fooNumber)}` + `${String((typeof window))}` + `${String(FooClass)}` + `${String(ClassWithToString)}` + `${String(classWithToString)}` + `${String(FooStr)}` + `${String(fooArr)}` + `${String(emptyArr)}` + `${String(booleanVar)}` + + `${'str literal'.toString()}` + `${123..toString()}` + `${fooAny.toString()}` + `${fooStr.toString()}` + `${stringUni.toString()}` + `${fooNumber.toString()}` + `${(typeof window).toString()}` + `${FooClass.toString()}` + `${ClassWithToString.toString()}` + `${classWithToString.toString()}` + `${FooStr.toString()}` + `${fooArr.toString()}` + `${emptyArr.toString()}` + `${booleanVar.toString()}` + + 'str' + 'str literal' + 'str' + 'str' + 123 + 'str' + 'str' + fooAny + 'str' + 'str' + fooStr + 'str' + 'str' + stringUni + 'str' + 'str' + fooNumber + 'str' + 'str' + (typeof window) + 'str' + 'str' + String(FooClass) + 'str' + 'str' + String(ClassWithToString) + 'str' + 'str' + String(classWithToString) + 'str' + 'str' + String(FooStr) + 'str' + 'str' + String(fooArr) + 'str' + 'str' + String(emptyArr) + 'str' + 'str' + booleanVar + 'str' + + 'str' + String('str literal') + 'str' + 'str' + String(123) + 'str' + 'str' + String(fooAny) + 'str' + 'str' + String(fooStr) + 'str' + 'str' + String(stringUni) + 'str' + 'str' + String(fooNumber) + 'str' + 'str' + String((typeof window)) + 'str' + 'str' + String(FooClass) + 'str' + 'str' + String(ClassWithToString) + 'str' + 'str' + String(classWithToString) + 'str' + 'str' + String(FooStr) + 'str' + 'str' + String(fooArr) + 'str' + 'str' + String(emptyArr) + 'str' + 'str' + String(booleanVar) + 'str' + + 'str' + 'str literal'.toString() + 'str' + 'str' + 123..toString() + 'str' + 'str' + fooAny.toString() + 'str' + 'str' + fooStr.toString() + 'str' + 'str' + stringUni.toString() + 'str' + 'str' + fooNumber.toString() + 'str' + 'str' + (typeof window).toString() + 'str' + 'str' + FooClass.toString() + 'str' + 'str' + ClassWithToString.toString() + 'str' + 'str' + classWithToString.toString() + 'str' + 'str' + FooStr.toString() + 'str' + 'str' + fooArr.toString() + 'str' + 'str' + emptyArr.toString() + 'str' + 'str' + booleanVar.toString() + 'str' + + const barFooStrOrUndef: string | undefined; + const barFooStrOrNull: string | null; + const neverType: never; + + `${String(barFooStrOrUndef)}` + `${String(barFooStrOrNull)}` + `${String(neverType)}` + + `${String(barFooStrOrUndef)}` + `${String(barFooStrOrNull)}` + `${String(neverType)}` + + `${barFooStrOrUndef.toString()}` + `${barFooStrOrNull.toString()}` + `${neverType.toString()}` + + 'str' + String(barFooStrOrUndef) + 'str' + 'str' + String(barFooStrOrNull) + 'str' + 'str' + String(neverType) + 'str' + + 'str' + String(barFooStrOrUndef) + 'str' + 'str' + String(barFooStrOrNull) + 'str' + 'str' + String(neverType) + 'str' + + 'str' + barFooStrOrUndef.toString() + 'str' + 'str' + barFooStrOrNull.toString() + 'str' + 'str' + neverType.toString() + 'str' diff --git a/test/rules/strict-string-expressions/disallow-empty-types/test.ts.lint b/test/rules/strict-string-expressions/disallow-empty-types/test.ts.lint new file mode 100644 index 00000000000..b7f29b9ed0c --- /dev/null +++ b/test/rules/strict-string-expressions/disallow-empty-types/test.ts.lint @@ -0,0 +1,151 @@ + const fooAny: any; + const fooStr: string = 'foo'; + const fooNumber = 2; + class FooClass {} + class ClassWithToString { + public static toString () { return ''; } + public toString () { return ''; } + } + const classWithToString = new ClassWithToString(); + const FooStr = new String('foo'); + const fooArr = ['foo']; + const emptyArr = []; + const stringUni = "foo" | "bar"; + const booleanVar: boolean; + + `foo` + `${'str literal'}` + `${123}` + `${fooAny}` + `${fooStr}` + `${stringUni}` + `${fooNumber}` + `${(typeof window)}` + `${FooClass}` + ~~~~~~~~~~ [Explicit conversion to string type required] + `${ClassWithToString}` + ~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + `${classWithToString}` + ~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + `${FooStr}` + ~~~~~~~~ [Explicit conversion to string type required] + `${fooArr}` + ~~~~~~~~ [Explicit conversion to string type required] + `${emptyArr}` + ~~~~~~~~~~ [Explicit conversion to string type required] + `${booleanVar}` + + `${String('str literal')}` + `${String(123)}` + `${String(fooAny)}` + `${String(fooStr)}` + `${String(stringUni)}` + `${String(fooNumber)}` + `${String((typeof window))}` + `${String(FooClass)}` + `${String(ClassWithToString)}` + `${String(classWithToString)}` + `${String(FooStr)}` + `${String(fooArr)}` + `${String(emptyArr)}` + `${String(booleanVar)}` + + `${'str literal'.toString()}` + `${123..toString()}` + `${fooAny.toString()}` + `${fooStr.toString()}` + `${stringUni.toString()}` + `${fooNumber.toString()}` + `${(typeof window).toString()}` + `${FooClass.toString()}` + `${ClassWithToString.toString()}` + `${classWithToString.toString()}` + `${FooStr.toString()}` + `${fooArr.toString()}` + `${emptyArr.toString()}` + `${booleanVar.toString()}` + + 'str' + 'str literal' + 'str' + 'str' + 123 + 'str' + 'str' + fooAny + 'str' + 'str' + fooStr + 'str' + 'str' + stringUni + 'str' + 'str' + fooNumber + 'str' + 'str' + (typeof window) + 'str' + 'str' + FooClass + 'str' + ~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + ClassWithToString + 'str' + ~~~~~~~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + classWithToString + 'str' + ~~~~~~~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + FooStr + 'str' + ~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + fooArr + 'str' + ~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + emptyArr + 'str' + ~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + booleanVar + 'str' + + 'str' + String('str literal') + 'str' + 'str' + String(123) + 'str' + 'str' + String(fooAny) + 'str' + 'str' + String(fooStr) + 'str' + 'str' + String(stringUni) + 'str' + 'str' + String(fooNumber) + 'str' + 'str' + String((typeof window)) + 'str' + 'str' + String(FooClass) + 'str' + 'str' + String(ClassWithToString) + 'str' + 'str' + String(classWithToString) + 'str' + 'str' + String(FooStr) + 'str' + 'str' + String(fooArr) + 'str' + 'str' + String(emptyArr) + 'str' + 'str' + String(booleanVar) + 'str' + + 'str' + 'str literal'.toString() + 'str' + 'str' + 123..toString() + 'str' + 'str' + fooAny.toString() + 'str' + 'str' + fooStr.toString() + 'str' + 'str' + stringUni.toString() + 'str' + 'str' + fooNumber.toString() + 'str' + 'str' + (typeof window).toString() + 'str' + 'str' + FooClass.toString() + 'str' + 'str' + ClassWithToString.toString() + 'str' + 'str' + classWithToString.toString() + 'str' + 'str' + FooStr.toString() + 'str' + 'str' + fooArr.toString() + 'str' + 'str' + emptyArr.toString() + 'str' + 'str' + booleanVar.toString() + 'str' + + const barFooStrOrUndef: string | undefined; + const barFooStrOrNull: string | null; + const neverType: never; + + `${barFooStrOrUndef}` + ~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + `${barFooStrOrNull}` + ~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + `${neverType}` + ~~~~~~~~~~~ [Explicit conversion to string type required] + + `${String(barFooStrOrUndef)}` + `${String(barFooStrOrNull)}` + `${String(neverType)}` + + `${barFooStrOrUndef.toString()}` + `${barFooStrOrNull.toString()}` + `${neverType.toString()}` + + 'str' + barFooStrOrUndef + 'str' + ~~~~~~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + barFooStrOrNull + 'str' + ~~~~~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + 'str' + neverType + 'str' + ~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required] + + 'str' + String(barFooStrOrUndef) + 'str' + 'str' + String(barFooStrOrNull) + 'str' + 'str' + String(neverType) + 'str' + + 'str' + barFooStrOrUndef.toString() + 'str' + 'str' + barFooStrOrNull.toString() + 'str' + 'str' + neverType.toString() + 'str' diff --git a/test/rules/strict-string-expressions/disallow-empty-types/tsconfig.json b/test/rules/strict-string-expressions/disallow-empty-types/tsconfig.json new file mode 100644 index 00000000000..c4d430eb449 --- /dev/null +++ b/test/rules/strict-string-expressions/disallow-empty-types/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} diff --git a/test/rules/strict-string-expressions/disallow-empty-types/tslint.json b/test/rules/strict-string-expressions/disallow-empty-types/tslint.json new file mode 100644 index 00000000000..265d57b2aaf --- /dev/null +++ b/test/rules/strict-string-expressions/disallow-empty-types/tslint.json @@ -0,0 +1,10 @@ +{ + "rules": { + "strict-string-expressions": [ + true, + { + "allow-empty-types": false + } + ] + } +}