Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.9k
feat(eslint-plugin): [no-useless-default-assignment] add rule#11720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Changes fromall commits
a97e52e4a8ef6b76374977a7b334527b367010c166ff7bd0b8d29d31df51d1c052606290d121a9b55fb2422a70416ff0d64c40bfbdef0e8a6bef7fa299249e242883a7d7a6366d7f339f5fb05cba7e074f64f7d703c49ff746f9f7a4d42f279b3f3989c230fcf4d87672cae2d39802f637ce84233c859e052981c3c04657071c2ef9334493be81a7fed0011c026fd7fFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| --- | ||
| description: 'Disallow default values that will never be used.' | ||
| --- | ||
| import Tabs from '@theme/Tabs'; | ||
| import TabItem from '@theme/TabItem'; | ||
| > 🛑 This file is source code, not the primary documentation location! 🛑 | ||
| > | ||
| > See **https://typescript-eslint.io/rules/no-useless-default-assignment** for documentation. | ||
| [Default parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters) and [default values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#default_value) are only used if the parameter or property is `undefined`. | ||
| That can happen when a value is missing, or when one is provided and set to `undefined`. | ||
| If a non-`undefined` value is guaranteed to be provided, then there is no need to define a default. | ||
| ## Examples | ||
| <Tabs> | ||
| <TabItem value="❌ Incorrect"> | ||
| ```ts | ||
| function Bar({ foo = '' }: { foo: string }) { | ||
| return foo; | ||
| } | ||
| const { foo = '' } = { foo: 'bar' }; | ||
| const [foo = ''] = ['bar']; | ||
| [1, 2, 3].map((a = 42) => a + 1); | ||
| ``` | ||
| </TabItem> | ||
| <TabItem value="✅ Correct"> | ||
| ```ts | ||
| function Bar({ foo = '' }: { foo?: string }) { | ||
| return foo; | ||
| } | ||
| const { foo = '' } = { foo: undefined }; | ||
| const [foo = ''] = [undefined]; | ||
| [1, 2, 3, undefined].map((a = 42) => a + 1); | ||
| ``` | ||
| </TabItem> | ||
| </Tabs> | ||
| ## When Not To Use It | ||
| If you use default values defensively against runtime values that bypass type checking, or for documentation purposes, you may want to disable this rule. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| import type { TSESTree } from '@typescript-eslint/utils'; | ||
| import { AST_NODE_TYPES } from '@typescript-eslint/utils'; | ||
| import * as tsutils from 'ts-api-utils'; | ||
| import * as ts from 'typescript'; | ||
| import { | ||
| createRule, | ||
| getParserServices, | ||
| isFunction, | ||
| isTypeAnyType, | ||
| isTypeFlagSet, | ||
| isTypeUnknownType, | ||
| nullThrows, | ||
| NullThrowsReasons, | ||
| } from '../util'; | ||
| type MessageId = 'uselessDefaultAssignment' | 'uselessUndefined'; | ||
| export default createRule<[], MessageId>({ | ||
| name: 'no-useless-default-assignment', | ||
| meta: { | ||
| type: 'suggestion', | ||
| docs: { | ||
| description: 'Disallow default values that will never be used', | ||
| recommended: 'strict', | ||
| requiresTypeChecking: true, | ||
ulrichstark marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| }, | ||
| fixable: 'code', | ||
| messages: { | ||
| uselessDefaultAssignment: | ||
| 'Default value is useless because the {{ type }} is not optional.', | ||
| uselessUndefined: | ||
| 'Default value is useless because it is undefined. Optional {{ type }}s are already undefined by default.', | ||
| }, | ||
| schema: [], | ||
| }, | ||
| defaultOptions: [], | ||
| create(context) { | ||
| const services = getParserServices(context); | ||
| const checker = services.program.getTypeChecker(); | ||
| const compilerOptions = services.program.getCompilerOptions(); | ||
| const isNoUncheckedIndexedAccess = tsutils.isCompilerOptionEnabled( | ||
| compilerOptions, | ||
| 'noUncheckedIndexedAccess', | ||
| ); | ||
| function canBeUndefined(type: ts.Type): boolean { | ||
| if (isTypeAnyType(type) || isTypeUnknownType(type)) { | ||
| return true; | ||
| } | ||
| return tsutils | ||
| .unionConstituents(type) | ||
| .some(part => isTypeFlagSet(part, ts.TypeFlags.Undefined)); | ||
| } | ||
| function getPropertyType( | ||
| objectType: ts.Type, | ||
| propertyName: string, | ||
| ): ts.Type | null { | ||
| const symbol = objectType.getProperty(propertyName); | ||
| if (!symbol) { | ||
| if (isNoUncheckedIndexedAccess) { | ||
| return null; | ||
| } | ||
| return objectType.getStringIndexType() ?? null; | ||
| } | ||
| return checker.getTypeOfSymbol(symbol); | ||
| } | ||
| function getArrayElementType( | ||
| arrayType: ts.Type, | ||
| elementIndex: number, | ||
| ): ts.Type | null { | ||
| if (checker.isTupleType(arrayType)) { | ||
| const tupleArgs = checker.getTypeArguments(arrayType); | ||
| if (elementIndex < tupleArgs.length) { | ||
| return tupleArgs[elementIndex]; | ||
| } | ||
| } | ||
| if (isNoUncheckedIndexedAccess) { | ||
| return null; | ||
| } | ||
| return arrayType.getNumberIndexType() ?? null; | ||
| } | ||
| function checkAssignmentPattern(node: TSESTree.AssignmentPattern): void { | ||
| if ( | ||
| node.right.type === AST_NODE_TYPES.Identifier && | ||
| node.right.name === 'undefined' | ||
| ) { | ||
| const type = | ||
| node.parent.type === AST_NODE_TYPES.Property || | ||
| node.parent.type === AST_NODE_TYPES.ArrayPattern | ||
| ? 'property' | ||
| : 'parameter'; | ||
| reportUselessDefault(node, type, 'uselessUndefined'); | ||
| return; | ||
| } | ||
| const parent = node.parent; | ||
| if ( | ||
| parent.type === AST_NODE_TYPES.ArrowFunctionExpression || | ||
| parent.type === AST_NODE_TYPES.FunctionExpression | ||
| ) { | ||
| const paramIndex = parent.params.indexOf(node); | ||
| if (paramIndex !== -1) { | ||
| const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); | ||
| if (ts.isFunctionLike(tsFunc)) { | ||
| const contextualType = checker.getContextualType( | ||
| tsFunc as ts.Expression, | ||
| ); | ||
| if (!contextualType) { | ||
| return; | ||
| } | ||
| const signatures = contextualType.getCallSignatures(); | ||
| const params = signatures[0].getParameters(); | ||
| if (paramIndex < params.length) { | ||
| const paramSymbol = params[paramIndex]; | ||
| if ((paramSymbol.flags & ts.SymbolFlags.Optional) === 0) { | ||
| const paramType = checker.getTypeOfSymbol(paramSymbol); | ||
| if (!canBeUndefined(paramType)) { | ||
| reportUselessDefault( | ||
| node, | ||
| 'parameter', | ||
| 'uselessDefaultAssignment', | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| if (parent.type === AST_NODE_TYPES.Property) { | ||
| const propertyType = getTypeOfProperty(parent); | ||
| if (!propertyType) { | ||
| return; | ||
| } | ||
| if (!canBeUndefined(propertyType)) { | ||
| reportUselessDefault(node, 'property', 'uselessDefaultAssignment'); | ||
| } | ||
| } else if (parent.type === AST_NODE_TYPES.ArrayPattern) { | ||
| const sourceType = getSourceTypeForPattern(parent); | ||
| if (!sourceType) { | ||
| return; | ||
| } | ||
| const elementIndex = parent.elements.indexOf(node); | ||
| const elementType = getArrayElementType(sourceType, elementIndex); | ||
| if (!elementType) { | ||
| return; | ||
| } | ||
| if (!canBeUndefined(elementType)) { | ||
| reportUselessDefault(node, 'property', 'uselessDefaultAssignment'); | ||
| } | ||
| } | ||
| } | ||
| function getTypeOfProperty(node: TSESTree.Property): ts.Type | null { | ||
| const objectPattern = node.parent as TSESTree.ObjectPattern; | ||
| const sourceType = getSourceTypeForPattern(objectPattern); | ||
| if (!sourceType) { | ||
| return null; | ||
| } | ||
| const propertyName = getPropertyName(node.key); | ||
| if (!propertyName) { | ||
| return null; | ||
| } | ||
| return getPropertyType(sourceType, propertyName); | ||
| } | ||
| function getSourceTypeForPattern(pattern: TSESTree.Node): ts.Type | null { | ||
| const parent = nullThrows( | ||
| pattern.parent, | ||
| NullThrowsReasons.MissingParent, | ||
| ); | ||
| if (parent.type === AST_NODE_TYPES.VariableDeclarator && parent.init) { | ||
| const tsNode = services.esTreeNodeToTSNodeMap.get(parent.init); | ||
| return checker.getTypeAtLocation(tsNode); | ||
| } | ||
| if (isFunction(parent)) { | ||
| const paramIndex = parent.params.indexOf(pattern as TSESTree.Parameter); | ||
| const tsFunc = services.esTreeNodeToTSNodeMap.get(parent); | ||
| const signature = nullThrows( | ||
| checker.getSignatureFromDeclaration(tsFunc), | ||
| NullThrowsReasons.MissingToken('signature', 'function'), | ||
| ); | ||
| const params = signature.getParameters(); | ||
| return checker.getTypeOfSymbol(params[paramIndex]); | ||
| } | ||
| if (parent.type === AST_NODE_TYPES.AssignmentPattern) { | ||
| return getSourceTypeForPattern(parent); | ||
| } | ||
| if (parent.type === AST_NODE_TYPES.Property) { | ||
| return getTypeOfProperty(parent); | ||
| } | ||
| if (parent.type === AST_NODE_TYPES.ArrayPattern) { | ||
| const arrayPattern = parent; | ||
| const arrayType = getSourceTypeForPattern(arrayPattern); | ||
| if (!arrayType) { | ||
| return null; | ||
| } | ||
| const elementIndex = arrayPattern.elements.indexOf( | ||
| pattern as TSESTree.DestructuringPattern, | ||
| ); | ||
| return getArrayElementType(arrayType, elementIndex); | ||
| } | ||
| return null; | ||
| } | ||
| function getPropertyName( | ||
| key: TSESTree.Expression | TSESTree.PrivateIdentifier, | ||
| ): string | null { | ||
| switch (key.type) { | ||
| case AST_NODE_TYPES.Identifier: | ||
| return key.name; | ||
| case AST_NODE_TYPES.Literal: | ||
| return String(key.value); | ||
| default: | ||
| return null; | ||
| } | ||
| } | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. [Non-Actionable] I'm surprised a utility doesn't already exist for this - but I couldn't find one.
Not requesting changes, just noting - maybe I missed one? | ||
| function reportUselessDefault( | ||
| node: TSESTree.AssignmentPattern, | ||
| type: 'parameter' | 'property', | ||
| messageId: MessageId, | ||
| ): void { | ||
| context.report({ | ||
| node: node.right, | ||
| messageId, | ||
| data: { type }, | ||
| fix(fixer) { | ||
| // Remove from before the = to the end of the default value | ||
| // Find the start position (including whitespace before =) | ||
| const start = node.left.range[1]; | ||
| const end = node.range[1]; | ||
| return fixer.removeRange([start, end]); | ||
| }, | ||
| }); | ||
| } | ||
| return { | ||
| AssignmentPattern: checkAssignmentPattern, | ||
| }; | ||
| }, | ||
| }); | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.