Skip to content
This repository has been archived by the owner on Mar 25, 2021. It is now read-only.

Add rule: strict-string-expressions #4807

Merged
merged 27 commits into from Aug 12, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
570a5d4
feat: add strict-string-expressions
ColCh Jul 22, 2019
928d5f7
docs(strictStringExpressionsRule): add option desc
ColCh Jul 22, 2019
fda5ea4
feat(strictStringExpressionsRule): add fixer
ColCh Jul 22, 2019
f7ddea6
test: add strict-string-expressions into all.ts
ColCh Jul 22, 2019
b2f9bc5
type: remove declare
ColCh Jul 22, 2019
e8a809a
refactor(strictStringExpressionsRule): eliminate copy-paste
ColCh Jul 22, 2019
5a9e7da
style: fix tslint errors
ColCh Jul 22, 2019
fa07d78
fix(strictStringExpressionsRule): exclude any type from checks
ColCh Jul 22, 2019
fe6332d
style: fix sytle erorrs
ColCh Jul 22, 2019
643268c
refactor: rename function name to more cohesive
ColCh Jul 22, 2019
2fafc18
refactor(strictStringExpressionsRule): add more renaimings
ColCh Jul 22, 2019
217229f
style(strictStringExpressionsRule): prettify
ColCh Jul 22, 2019
235d7aa
fix: do not require numbers to be stringified
ColCh Jul 22, 2019
589f06f
test(strictStringExpressions): add string and number literals
ColCh Jul 22, 2019
6259432
test(strictStringExpressionsRule): fix nit
ColCh Jul 22, 2019
88a2278
test(strictStringExpressionsRule): add string uni in test
ColCh Jul 22, 2019
7d8253d
test: add typeof window case
ColCh Jul 28, 2019
ae1c2be
feat: handle union type
ColCh Jul 28, 2019
7de511b
feat(strictStringExpressionsRule): add allow-empty-types option
ColCh Jul 28, 2019
bb841e0
feat: allo empty types in all.ts config
ColCh Jul 28, 2019
2c9603e
style(strictStringExpressionsRule): fix lint rules
ColCh Jul 28, 2019
cf3f90d
feat(strictStringExpressionsRule): allow booleans
ColCh Jul 28, 2019
6ee97af
style(linter): fix codestyle
ColCh Jul 28, 2019
c460673
refactor(strictStringExpressionsRule): rename private helpers
ColCh Jul 28, 2019
b99df8d
docs(strictStringExpressionsRule): update license year
ColCh Jul 28, 2019
2ac3f27
refactor(strictStringExpressionsRule): make allow-empty-types to appear
ColCh Jul 28, 2019
bb95c45
test(stringStrictExpressions): correct test
ColCh Jul 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/configs/all.ts
Expand Up @@ -150,6 +150,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,
Expand Down
93 changes: 93 additions & 0 deletions src/rules/strictStringExpressionsRule.ts
@@ -0,0 +1,93 @@
/**
* @license
* Copyright 2018 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 * as ts from "typescript";

import * as Lint from "../index";
import { isTypeFlagSet } from 'tsutils';

export class Rule extends Lint.Rules.TypedRule {
public static metadata: Lint.IRuleMetadata = {
ruleName: "strict-string-expressions",
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}\`)`,
type: "functionality",
typescriptOnly: true,
requiresTypeInfo: true,
options: [],
optionExamples: [true],
optionsDescription: "Not configurable.",
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
hasFix: true
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
};

public static CONVERSION_REQUIRED = 'Explicit conversion to string type required';
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved

public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk, undefined, program.getTypeChecker());
}
}

function walk(ctx: Lint.WalkContext<undefined>, checker: ts.TypeChecker): void {
const { sourceFile } = ctx;
ts.forEachChild(sourceFile, function cb(node: ts.Node): void {
switch (node.kind) {
case ts.SyntaxKind.BinaryExpression: {
ColCh marked this conversation as resolved.
Show resolved Hide resolved
const binaryExpr = node as ts.BinaryExpression;
if (binaryExpr.operatorToken.kind === ts.SyntaxKind.PlusToken) {
const leftIsString = isTypeFlagSet(checker.getTypeAtLocation(binaryExpr.left), ts.TypeFlags.StringLike);
const rightIsString = isTypeFlagSet(checker.getTypeAtLocation(binaryExpr.right), ts.TypeFlags.StringLike);
const leftIsFailed = !leftIsString && rightIsString;
const rightIsFailed = leftIsString && !rightIsString;
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 isString = isTypeFlagSet(type, ts.TypeFlags.StringLike);
if (!isString) {
const { expression } = templateSpanNode;
addFailure(templateSpanNode, expression);
}
break;
}
}
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);
}
}
87 changes: 87 additions & 0 deletions test/rules/strict-string-expressions/test.ts.fix
@@ -0,0 +1,87 @@
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 = [];

`foo`
`${fooStr}`
`${String(fooNumber)}`
`${String(FooClass)}`
`${String(ClassWithToString)}`
`${String(classWithToString)}`
`${String(FooStr)}`
`${String(fooArr)}`
`${String(emptyArr)}`

`${String(fooStr)}`
`${String(fooNumber)}`
`${String(FooClass)}`
`${String(ClassWithToString)}`
`${String(classWithToString)}`
`${String(FooStr)}`
`${String(fooArr)}`
`${String(emptyArr)}`

`${fooStr.toString()}`
`${fooNumber.toString()}`
`${FooClass.toString()}`
`${ClassWithToString.toString()}`
`${classWithToString.toString()}`
`${FooStr.toString()}`
`${fooArr.toString()}`
`${emptyArr.toString()}`

'str' + fooStr + 'str'
'str' + String(fooNumber) + '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(fooStr) + 'str'
'str' + String(fooNumber) + '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' + fooStr.toString() + 'str'
'str' + fooNumber.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;

`${String(barFooStrOrUndef)}`
`${String(barFooStrOrNull)}`

`${String(barFooStrOrUndef)}`
`${String(barFooStrOrNull)}`

`${barFooStrOrUndef.toString()}`
`${barFooStrOrNull.toString()}`

'str' + String(barFooStrOrUndef) + 'str'
'str' + String(barFooStrOrNull) + 'str'

'str' + String(barFooStrOrUndef) + 'str'
'str' + String(barFooStrOrNull) + 'str'

'str' + barFooStrOrUndef.toString() + 'str'
'str' + barFooStrOrNull.toString() + 'str'
105 changes: 105 additions & 0 deletions test/rules/strict-string-expressions/test.ts.lint
@@ -0,0 +1,105 @@
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 = [];

`foo`
`${fooStr}`
`${fooNumber}`
~~~~~~~~~~~ [Explicit conversion to string type required]
`${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(fooStr)}`
`${String(fooNumber)}`
`${String(FooClass)}`
`${String(ClassWithToString)}`
`${String(classWithToString)}`
`${String(FooStr)}`
`${String(fooArr)}`
`${String(emptyArr)}`

`${fooStr.toString()}`
`${fooNumber.toString()}`
`${FooClass.toString()}`
`${ClassWithToString.toString()}`
`${classWithToString.toString()}`
`${FooStr.toString()}`
`${fooArr.toString()}`
`${emptyArr.toString()}`

'str' + fooStr + 'str'
'str' + fooNumber + 'str'
~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required]
'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(fooStr) + 'str'
'str' + String(fooNumber) + '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' + fooStr.toString() + 'str'
'str' + fooNumber.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;

`${barFooStrOrUndef}`
~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required]
`${barFooStrOrNull}`
~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required]

`${String(barFooStrOrUndef)}`
`${String(barFooStrOrNull)}`

`${barFooStrOrUndef.toString()}`
`${barFooStrOrNull.toString()}`

'str' + barFooStrOrUndef + 'str'
~~~~~~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required]
'str' + barFooStrOrNull + 'str'
~~~~~~~~~~~~~~~~~~~~~~~ [Explicit conversion to string type required]

'str' + String(barFooStrOrUndef) + 'str'
'str' + String(barFooStrOrNull) + 'str'

'str' + barFooStrOrUndef.toString() + 'str'
'str' + barFooStrOrNull.toString() + 'str'
5 changes: 5 additions & 0 deletions test/rules/strict-string-expressions/tsconfig.json
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"strictNullChecks": true
}
}
5 changes: 5 additions & 0 deletions test/rules/strict-string-expressions/tslint.json
@@ -0,0 +1,5 @@
{
"rules": {
"strict-string-expressions": true
}
}