This repository has been archived by the owner on Mar 25, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 887
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
11 changed files
with
752 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Options>, 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) | ||
); | ||
} |
133 changes: 133 additions & 0 deletions
133
test/rules/strict-string-expressions/allow-empty-types/test.ts.fix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' |
Oops, something went wrong.