1import { 2 TSESTree, 3 AST_NODE_TYPES, 4} from '@typescript-eslint/experimental-utils'; 5import { 6 isObjectType, 7 isObjectFlagSet, 8 isStrictCompilerOptionEnabled, 9 isTypeFlagSet, 10 isVariableDeclaration, 11} from 'tsutils'; 12import * as ts from 'typescript'; 13import * as util from '../util'; 14 15type Options = [ 16 { 17 typesToIgnore?: string[]; 18 }, 19]; 20type MessageIds = 'contextuallyUnnecessary' | 'unnecessaryAssertion'; 21 22export default util.createRule<Options, MessageIds>({ 23 name: 'no-unnecessary-type-assertion', 24 meta: { 25 docs: { 26 description: 27 'Warns if a type assertion does not change the type of an expression', 28 category: 'Best Practices', 29 recommended: 'error', 30 requiresTypeChecking: true, 31 }, 32 fixable: 'code', 33 messages: { 34 unnecessaryAssertion: 35 'This assertion is unnecessary since it does not change the type of the expression.', 36 contextuallyUnnecessary: 37 'This assertion is unnecessary since the receiver accepts the original type of the expression.', 38 }, 39 schema: [ 40 { 41 type: 'object', 42 properties: { 43 typesToIgnore: { 44 type: 'array', 45 items: { 46 type: 'string', 47 }, 48 }, 49 }, 50 }, 51 ], 52 type: 'suggestion', 53 }, 54 defaultOptions: [{}], 55 create(context, [options]) { 56 const sourceCode = context.getSourceCode(); 57 const parserServices = util.getParserServices(context); 58 const checker = parserServices.program.getTypeChecker(); 59 const compilerOptions = parserServices.program.getCompilerOptions(); 60 61 /** 62 * Sometimes tuple types don't have ObjectFlags.Tuple set, like when they're being matched against an inferred type. 63 * So, in addition, check if there are integer properties 0..n and no other numeric keys 64 */ 65 function couldBeTupleType(type: ts.ObjectType): boolean { 66 const properties = type.getProperties(); 67 68 if (properties.length === 0) { 69 return false; 70 } 71 let i = 0; 72 73 for (; i < properties.length; ++i) { 74 const name = properties[i].name; 75 76 if (String(i) !== name) { 77 if (i === 0) { 78 // if there are no integer properties, this is not a tuple 79 return false; 80 } 81 break; 82 } 83 } 84 for (; i < properties.length; ++i) { 85 if (String(+properties[i].name) === properties[i].name) { 86 return false; // if there are any other numeric properties, this is not a tuple 87 } 88 } 89 return true; 90 } 91 92 /** 93 * Returns true if there's a chance the variable has been used before a value has been assigned to it 94 */ 95 function isPossiblyUsedBeforeAssigned(node: ts.Expression): boolean { 96 const declaration = util.getDeclaration(checker, node); 97 if (!declaration) { 98 // don't know what the declaration is for some reason, so just assume the worst 99 return true; 100 } 101 102 if ( 103 // non-strict mode doesn't care about used before assigned errors 104 isStrictCompilerOptionEnabled(compilerOptions, 'strictNullChecks') && 105 // ignore class properties as they are compile time guarded 106 // also ignore function arguments as they can't be used before defined 107 isVariableDeclaration(declaration) && 108 // is it `const x!: number` 109 declaration.initializer === undefined && 110 declaration.exclamationToken === undefined && 111 declaration.type !== undefined 112 ) { 113 // check if the defined variable type has changed since assignment 114 const declarationType = checker.getTypeFromTypeNode(declaration.type); 115 const type = util.getConstrainedTypeAtLocation(checker, node); 116 if (declarationType === type) { 117 // possibly used before assigned, so just skip it 118 // better to false negative and skip it, than false positive and fix to compile erroring code 119 // 120 // no better way to figure this out right now 121 // https://github.com/Microsoft/TypeScript/issues/31124 122 return true; 123 } 124 } 125 return false; 126 } 127 128 function isConstAssertion(node: TSESTree.TypeNode): boolean { 129 return ( 130 node.type === AST_NODE_TYPES.TSTypeReference && 131 node.typeName.type === AST_NODE_TYPES.Identifier && 132 node.typeName.name === 'const' 133 ); 134 } 135 136 return { 137 TSNonNullExpression(node): void { 138 const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); 139 const type = util.getConstrainedTypeAtLocation( 140 checker, 141 originalNode.expression, 142 ); 143 144 if (!util.isNullableType(type)) { 145 if (isPossiblyUsedBeforeAssigned(originalNode.expression)) { 146 return; 147 } 148 149 context.report({ 150 node, 151 messageId: 'unnecessaryAssertion', 152 fix(fixer) { 153 return fixer.removeRange([ 154 originalNode.expression.end, 155 originalNode.end, 156 ]); 157 }, 158 }); 159 } else { 160 // we know it's a nullable type 161 // so figure out if the variable is used in a place that accepts nullable types 162 163 const contextualType = util.getContextualType(checker, originalNode); 164 if (contextualType) { 165 // in strict mode you can't assign null to undefined, so we have to make sure that 166 // the two types share a nullable type 167 const typeIncludesUndefined = util.isTypeFlagSet( 168 type, 169 ts.TypeFlags.Undefined, 170 ); 171 const typeIncludesNull = util.isTypeFlagSet( 172 type, 173 ts.TypeFlags.Null, 174 ); 175 176 const contextualTypeIncludesUndefined = util.isTypeFlagSet( 177 contextualType, 178 ts.TypeFlags.Undefined, 179 ); 180 const contextualTypeIncludesNull = util.isTypeFlagSet( 181 contextualType, 182 ts.TypeFlags.Null, 183 ); 184 185 // make sure that the parent accepts the same types 186 // i.e. assigning `string | null | undefined` to `string | undefined` is invalid 187 const isValidUndefined = typeIncludesUndefined 188 ? contextualTypeIncludesUndefined 189 : true; 190 const isValidNull = typeIncludesNull 191 ? contextualTypeIncludesNull 192 : true; 193 194 if (isValidUndefined && isValidNull) { 195 context.report({ 196 node, 197 messageId: 'contextuallyUnnecessary', 198 fix(fixer) { 199 return fixer.removeRange([ 200 originalNode.expression.end, 201 originalNode.end, 202 ]); 203 }, 204 }); 205 } 206 } 207 } 208 }, 209 'TSAsExpression, TSTypeAssertion'( 210 node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, 211 ): void { 212 if ( 213 options.typesToIgnore?.includes( 214 sourceCode.getText(node.typeAnnotation), 215 ) || 216 isConstAssertion(node.typeAnnotation) 217 ) { 218 return; 219 } 220 221 const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); 222 const castType = checker.getTypeAtLocation(originalNode); 223 224 if ( 225 isTypeFlagSet(castType, ts.TypeFlags.Literal) || 226 (isObjectType(castType) && 227 (isObjectFlagSet(castType, ts.ObjectFlags.Tuple) || 228 couldBeTupleType(castType))) 229 ) { 230 // It's not always safe to remove a cast to a literal type or tuple 231 // type, as those types are sometimes widened without the cast. 232 return; 233 } 234 235 const uncastType = checker.getTypeAtLocation(originalNode.expression); 236 237 if (uncastType === castType) { 238 context.report({ 239 node, 240 messageId: 'unnecessaryAssertion', 241 fix(fixer) { 242 return originalNode.kind === ts.SyntaxKind.TypeAssertionExpression 243 ? fixer.removeRange([ 244 node.range[0], 245 node.expression.range[0] - 1, 246 ]) 247 : fixer.removeRange([ 248 node.expression.range[1] + 1, 249 node.range[1], 250 ]); 251 }, 252 }); 253 } 254 255 // TODO - add contextually unnecessary check for this 256 }, 257 }; 258 }, 259}); 260