Skip to content

Commit 1988e50

Browse files
committed
Add noUnnecessaryCasts compiler option, quickfix
1 parent b4715d3 commit 1988e50

File tree

31 files changed

+846
-25
lines changed

31 files changed

+846
-25
lines changed

src/compiler/checker.ts

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19882,6 +19882,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1988219882
}
1988319883
}
1988419884
if (sourceFlags & TypeFlags.Conditional) {
19885+
if ((source as ConditionalType).root === (target as ConditionalType).root) {
19886+
// Two instantiations of the same conditional type, just check instantiated outer type parameter equality
19887+
const params = (source as ConditionalType).root.outerTypeParameters || [];
19888+
const sourceTypeArguments = map(params, t => (source as ConditionalType).mapper ? getMappedType(t, (source as ConditionalType).mapper!) : t);
19889+
const targetTypeArguments = map(params, t => (target as ConditionalType).mapper ? getMappedType(t, (target as ConditionalType).mapper!) : t);
19890+
return typeArgumentsRelatedTo(sourceTypeArguments, targetTypeArguments, map(params, () => VarianceFlags.Unmeasurable), /*reportErrors*/ false, IntersectionState.None);
19891+
}
1988519892
if ((source as ConditionalType).root.isDistributive === (target as ConditionalType).root.isDistributive) {
1988619893
if (result = isRelatedTo((source as ConditionalType).checkType, (target as ConditionalType).checkType, RecursionFlags.Both, /*reportErrors*/ false)) {
1988719894
if (result &= isRelatedTo((source as ConditionalType).extendsType, (target as ConditionalType).extendsType, RecursionFlags.Both, /*reportErrors*/ false)) {
@@ -26071,6 +26078,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2607126078
node.kind === SyntaxKind.PropertyDeclaration)!;
2607226079
}
2607326080

26081+
function getControlFlowContainerForIdentifier(node: Identifier, declaration: Declaration, symbol: Symbol, typeFromSymbol: Type) {
26082+
// The declaration container is the innermost function that encloses the declaration of the variable
26083+
// or parameter. The flow container is the innermost function starting with which we analyze the control
26084+
// flow graph to determine the control flow based type.
26085+
const isParameter = getRootDeclaration(declaration).kind === SyntaxKind.Parameter;
26086+
const declarationContainer = getControlFlowContainer(declaration);
26087+
let flowContainer = getControlFlowContainer(node);
26088+
// When the control flow originates in a function expression or arrow function and we are referencing
26089+
// a const variable or parameter from an outer function, we extend the origin of the control flow
26090+
// analysis to include the immediately enclosing function.
26091+
while (flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression ||
26092+
flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) &&
26093+
(isConstVariable(symbol) && typeFromSymbol !== autoArrayType || isParameter && !isSymbolAssigned(symbol))) {
26094+
flowContainer = getControlFlowContainer(flowContainer);
26095+
}
26096+
return flowContainer;
26097+
}
26098+
2607426099
// Check if a parameter or catch variable is assigned anywhere
2607526100
function isSymbolAssigned(symbol: Symbol) {
2607626101
if (!symbol.valueDeclaration) {
@@ -26432,24 +26457,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2643226457
}
2643326458

2643426459
type = getNarrowableTypeForReference(type, node, checkMode);
26435-
26436-
// The declaration container is the innermost function that encloses the declaration of the variable
26437-
// or parameter. The flow container is the innermost function starting with which we analyze the control
26438-
// flow graph to determine the control flow based type.
2643926460
const isParameter = getRootDeclaration(declaration).kind === SyntaxKind.Parameter;
2644026461
const declarationContainer = getControlFlowContainer(declaration);
26441-
let flowContainer = getControlFlowContainer(node);
26442-
const isOuterVariable = flowContainer !== declarationContainer;
26443-
const isSpreadDestructuringAssignmentTarget = node.parent && node.parent.parent && isSpreadAssignment(node.parent) && isDestructuringAssignmentTarget(node.parent.parent);
26462+
const flowContainer = getControlFlowContainerForIdentifier(node, declaration, localOrExportSymbol, type);
2644426463
const isModuleExports = symbol.flags & SymbolFlags.ModuleExports;
26445-
// When the control flow originates in a function expression or arrow function and we are referencing
26446-
// a const variable or parameter from an outer function, we extend the origin of the control flow
26447-
// analysis to include the immediately enclosing function.
26448-
while (flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression ||
26449-
flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) &&
26450-
(isConstVariable(localOrExportSymbol) && type !== autoArrayType || isParameter && !isSymbolAssigned(localOrExportSymbol))) {
26451-
flowContainer = getControlFlowContainer(flowContainer);
26452-
}
26464+
const isOuterVariable = getControlFlowContainer(node) !== declarationContainer;
26465+
const isSpreadDestructuringAssignmentTarget = node.parent && node.parent.parent && isSpreadAssignment(node.parent) && isDestructuringAssignmentTarget(node.parent.parent);
2645326466
// We only look for uninitialized variables in strict null checking mode, and only when we can analyze
2645426467
// the entire control flow graph from the variable's declaration (i.e. when the flow container and
2645526468
// declaration container are the same).
@@ -32762,23 +32775,29 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3276232775
}
3276332776

3276432777
function checkAssertionWorker(errNode: Node, type: TypeNode, expression: UnaryExpression | Expression, checkMode?: CheckMode) {
32765-
let exprType = checkExpression(expression, checkMode);
32778+
const exprType = checkExpression(expression, checkMode);
3276632779
if (isConstTypeReference(type)) {
3276732780
if (!isValidConstAssertionArgument(expression)) {
3276832781
error(expression, Diagnostics.A_const_assertions_can_only_be_applied_to_references_to_enum_members_or_string_number_boolean_array_or_object_literals);
3276932782
}
3277032783
return getRegularTypeOfLiteralType(exprType);
3277132784
}
3277232785
checkSourceElement(type);
32773-
exprType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(exprType));
3277432786
const targetType = getTypeFromTypeNode(type);
3277532787
if (!isErrorType(targetType)) {
3277632788
addLazyDiagnostic(() => {
32777-
const widenedType = getWidenedType(exprType);
32789+
const regularType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(exprType));
32790+
const widenedType = getWidenedType(regularType);
3277832791
if (!isTypeComparableTo(targetType, widenedType)) {
32779-
checkTypeComparableTo(exprType, targetType, errNode,
32792+
checkTypeComparableTo(regularType, targetType, errNode,
3278032793
Diagnostics.Conversion_of_type_0_to_type_1_may_be_a_mistake_because_neither_type_sufficiently_overlaps_with_the_other_If_this_was_intentional_convert_the_expression_to_unknown_first);
3278132794
}
32795+
// Assertions, as a side effect, disable widening - some casts may rely on this, so
32796+
// if the expression type was widenable, we shouldn't assume the cast is extraneous.
32797+
// (Generally speaking, such casts are better served with `as const` casts nowadays though!)
32798+
else if (widenedType === exprType && isTypeIdenticalTo(targetType, widenedType)) {
32799+
errorOrSuggestion(!!compilerOptions.noUnnecessaryCasts, errNode, Diagnostics.Type_cast_has_no_effect_on_the_type_of_this_expression);
32800+
}
3278232801
});
3278332802
}
3278432803
return targetType;
@@ -32791,8 +32810,37 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3279132810
}
3279232811

3279332812
function checkNonNullAssertion(node: NonNullExpression) {
32794-
return node.flags & NodeFlags.OptionalChain ? checkNonNullChain(node as NonNullChain) :
32795-
getNonNullableType(checkExpression(node.expression));
32813+
const exprType = checkExpression(node.expression);
32814+
const resultType = node.flags & NodeFlags.OptionalChain ? checkNonNullChain(node as NonNullChain) : getNonNullableType(exprType);
32815+
if (!isErrorType(resultType)) {
32816+
addLazyDiagnostic(() => {
32817+
if (isTypeIdenticalTo(exprType, resultType)) {
32818+
// A non-null assertion on an identifier may also suppress a used-before definition, so may still be useful even if the type is unchanged by it
32819+
// Identifiers on the left-hand-side of an assignment expression, ofc, can't do this, since they're for the assigned type itself.
32820+
if (isIdentifier(node.expression) && !containsUndefinedType(exprType) && !(isAssignmentExpression(node.parent) && node.parent.left === node)) {
32821+
const symbol = getSymbolAtLocation(node.expression);
32822+
const declaration = symbol?.valueDeclaration;
32823+
if (declaration) {
32824+
const isParameter = getRootDeclaration(declaration).kind === SyntaxKind.Parameter;
32825+
if (!isParameter) {
32826+
const flowContainer = getControlFlowContainerForIdentifier(node.expression, declaration, symbol, exprType);
32827+
// We need a fake reference that isn't parented to the nonnull expression, since the non-null will affect control flow's result
32828+
// by suppressing the initial return type at the end of the flow
32829+
const fakeReference = factory.cloneNode(node.expression);
32830+
setParent(fakeReference, node.parent);
32831+
fakeReference.flowNode = node.expression.flowNode;
32832+
const flowType = getFlowTypeOfReference(fakeReference, exprType, undefinedType, flowContainer);
32833+
if (containsUndefinedType(flowType)) {
32834+
return; // Cast is required to suppress a use before assignment control flow error
32835+
}
32836+
}
32837+
}
32838+
}
32839+
errorOrSuggestion(!!compilerOptions.noUnnecessaryCasts, node, Diagnostics.Type_cast_has_no_effect_on_the_type_of_this_expression);
32840+
}
32841+
});
32842+
}
32843+
return resultType;
3279632844
}
3279732845

3279832846
function checkExpressionWithTypeArguments(node: ExpressionWithTypeArguments | TypeQueryNode) {

src/compiler/commandLineParser.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,15 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
800800
description: Diagnostics.Raise_an_error_when_a_function_parameter_isn_t_read,
801801
defaultValueDescription: false,
802802
},
803+
{
804+
name: "noUnnecessaryCasts",
805+
type: "boolean",
806+
affectsSemanticDiagnostics: true,
807+
affectsBuildInfo: true,
808+
category: Diagnostics.Type_Checking,
809+
description: Diagnostics.Raise_an_error_when_a_type_cast_does_not_affect_the_type_of_an_expression,
810+
defaultValueDescription: false,
811+
},
803812
{
804813
name: "exactOptionalPropertyTypes",
805814
type: "boolean",

src/compiler/diagnosticMessages.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5919,6 +5919,10 @@
59195919
"category": "Message",
59205920
"code": 6803
59215921
},
5922+
"Raise an error when a type cast does not affect the type of an expression.": {
5923+
"category": "Message",
5924+
"code": 6804
5925+
},
59225926

59235927
"one of:": {
59245928
"category": "Message",
@@ -6534,6 +6538,10 @@
65346538
"category": "Suggestion",
65356539
"code": 80008
65366540
},
6541+
"Type cast has no effect on the type of this expression.": {
6542+
"category": "Error",
6543+
"code": 80009
6544+
},
65376545

65386546
"Add missing 'super()' call": {
65396547
"category": "Message",
@@ -7392,6 +7400,14 @@
73927400
"category": "Message",
73937401
"code": 95175
73947402
},
7403+
"Remove unnecessary type cast.": {
7404+
"category": "Message",
7405+
"code": 95176
7406+
},
7407+
"Remove all unnecessary type casts.": {
7408+
"category": "Message",
7409+
"code": 95177
7410+
},
73957411

73967412
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
73977413
"category": "Error",

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6713,6 +6713,7 @@ export interface CompilerOptions {
67136713
noImplicitReturns?: boolean;
67146714
noImplicitThis?: boolean; // Always combine with strict property
67156715
noStrictGenericChecks?: boolean;
6716+
noUnnecessaryCasts?: boolean;
67166717
noUnusedLocals?: boolean;
67176718
noUnusedParameters?: boolean;
67186719
noImplicitUseStrict?: boolean;

src/services/_namespaces/ts.codefix.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export * from "../codefixes/wrapJsxInFragment";
6666
export * from "../codefixes/convertToMappedObjectType";
6767
export * from "../codefixes/removeAccidentalCallParentheses";
6868
export * from "../codefixes/removeUnnecessaryAwait";
69+
export * from "../codefixes/removeUnnecessaryCast";
6970
export * from "../codefixes/splitTypeOnlyImport";
7071
export * from "../codefixes/convertConstToLet";
7172
export * from "../codefixes/fixExpectedComma";
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
Diagnostics, findAncestor, isAssertionExpression, isInJSDoc, Node,
3+
isParenthesizedExpression, ParenthesizedExpression, SourceFile, textChanges, TextSpan, tryCast, HasJSDoc, first, last, needsParentheses, isNonNullExpression, AssertionExpression, NonNullExpression,
4+
} from "../_namespaces/ts";
5+
import { codeFixAll, createCodeFixAction, findAncestorMatchingSpan, registerCodeFix } from "../_namespaces/ts.codefix";
6+
7+
const fixId = "removeUnnecessaryCast";
8+
const errorCodes = [
9+
Diagnostics.Type_cast_has_no_effect_on_the_type_of_this_expression.code,
10+
];
11+
12+
registerCodeFix({
13+
errorCodes,
14+
getCodeActions: function getCodeActionsToRemoveUnnecessaryCast(context) {
15+
const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span));
16+
if (changes.length > 0) {
17+
return [createCodeFixAction(fixId, changes, Diagnostics.Remove_unnecessary_type_cast, fixId, Diagnostics.Remove_all_unnecessary_type_casts)];
18+
}
19+
},
20+
fixIds: [fixId],
21+
getAllCodeActions: context => {
22+
return codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag));
23+
},
24+
});
25+
26+
function makeChange(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, span: TextSpan) {
27+
let node: Node | undefined = findAncestorMatchingSpan(sourceFile, span);
28+
if (node && isInJSDoc(node)) {
29+
node = findAncestor(node, isParenthesizedExpression);
30+
if (node) {
31+
changeTracker.deleteNodeRange(sourceFile, first((node as HasJSDoc).jsDoc!), last((node as HasJSDoc).jsDoc!));
32+
if (!needsParentheses((node as ParenthesizedExpression).expression)) {
33+
changeTracker.replaceNode(sourceFile, node, (node as ParenthesizedExpression).expression);
34+
}
35+
}
36+
return;
37+
}
38+
const castExpr = tryCast(node, (n): n is AssertionExpression | NonNullExpression => isAssertionExpression(n) || isNonNullExpression(n));
39+
if (!castExpr) {
40+
return;
41+
}
42+
43+
changeTracker.replaceNode(sourceFile, castExpr, castExpr.expression);
44+
}

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7120,6 +7120,7 @@ declare namespace ts {
71207120
noImplicitReturns?: boolean;
71217121
noImplicitThis?: boolean;
71227122
noStrictGenericChecks?: boolean;
7123+
noUnnecessaryCasts?: boolean;
71237124
noUnusedLocals?: boolean;
71247125
noUnusedParameters?: boolean;
71257126
noImplicitUseStrict?: boolean;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3182,6 +3182,7 @@ declare namespace ts {
31823182
noImplicitReturns?: boolean;
31833183
noImplicitThis?: boolean;
31843184
noStrictGenericChecks?: boolean;
3185+
noUnnecessaryCasts?: boolean;
31853186
noUnusedLocals?: boolean;
31863187
noUnusedParameters?: boolean;
31873188
noImplicitUseStrict?: boolean;

tests/baselines/reference/config/initTSConfig/Default initialized TSConfig/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
8888
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
8989
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90+
// "noUnnecessaryCasts": true, /* Raise an error when a type cast does not affect the type of an expression. */
9091
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
9192
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
9293
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */

tests/baselines/reference/config/initTSConfig/Initialized TSConfig with --help/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
8888
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
8989
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90+
// "noUnnecessaryCasts": true, /* Raise an error when a type cast does not affect the type of an expression. */
9091
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
9192
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
9293
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */

tests/baselines/reference/config/initTSConfig/Initialized TSConfig with --watch/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
8888
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
8989
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90+
// "noUnnecessaryCasts": true, /* Raise an error when a type cast does not affect the type of an expression. */
9091
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
9192
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
9293
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */

tests/baselines/reference/config/initTSConfig/Initialized TSConfig with advanced options/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
8888
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
8989
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
90+
// "noUnnecessaryCasts": true, /* Raise an error when a type cast does not affect the type of an expression. */
9091
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
9192
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
9293
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */

0 commit comments

Comments
 (0)