diff --git a/its/ruling/src/test/expected/ant-design/typescript-S4782.json b/its/ruling/src/test/expected/ant-design/typescript-S4782.json index 7f8b2d5125b..5691b42b49b 100644 --- a/its/ruling/src/test/expected/ant-design/typescript-S4782.json +++ b/its/ruling/src/test/expected/ant-design/typescript-S4782.json @@ -1,5 +1,83 @@ { +"ant-design:components/button/button-group.tsx": [ +8 +], +"ant-design:components/button/button.tsx": [ +105 +], +"ant-design:components/cascader/index.tsx": [ +104 +], +"ant-design:components/collapse/Collapse.tsx": [ +32 +], +"ant-design:components/config-provider/DisabledContext.tsx": [ +8 +], +"ant-design:components/config-provider/SizeContext.tsx": [ +8 +], +"ant-design:components/config-provider/context.tsx": [ +41 +], +"ant-design:components/config-provider/index.tsx": [ +83, +85 +], +"ant-design:components/date-picker/generatePicker/index.tsx": [ +82 +], "ant-design:components/drawer/index.tsx": [ 66 +], +"ant-design:components/form/Form.tsx": [ +31 +], +"ant-design:components/input-number/index.tsx": [ +25 +], +"ant-design:components/input/ClearableLabeledInput.tsx": [ +31, +40 +], +"ant-design:components/input/Input.tsx": [ +117 +], +"ant-design:components/input/TextArea.tsx": [ +51 +], +"ant-design:components/menu/MenuContext.tsx": [ +10 +], +"ant-design:components/modal/Modal.tsx": [ +124 +], +"ant-design:components/progress/Line.tsx": [ +9 +], +"ant-design:components/radio/interface.tsx": [ +14, +15 +], +"ant-design:components/segmented/index.tsx": [ +41 +], +"ant-design:components/select/index.tsx": [ +40 +], +"ant-design:components/table/Table.tsx": [ +88 +], +"ant-design:components/tabs/index.tsx": [ +22 +], +"ant-design:components/transfer/operation.tsx": [ +17 +], +"ant-design:components/tree-select/index.tsx": [ +50 +], +"ant-design:components/typography/Editable.tsx": [ +19 ] } diff --git a/its/ruling/src/test/expected/eigen/typescript-S4782.json b/its/ruling/src/test/expected/eigen/typescript-S4782.json index 09987b06996..7296339a9ed 100644 --- a/its/ruling/src/test/expected/eigen/typescript-S4782.json +++ b/its/ruling/src/test/expected/eigen/typescript-S4782.json @@ -1,4 +1,7 @@ { +"eigen:src/app/Scenes/Artwork/Components/ImageCarousel/ImageCarousel.tsx": [ +34 +], "eigen:src/app/Scenes/Fair/Components/FairArtworks.tsx": [ 25 ], diff --git a/its/ruling/src/test/expected/vitest/typescript-S4782.json b/its/ruling/src/test/expected/vitest/typescript-S4782.json index 79aa0abf2d9..081511c4580 100644 --- a/its/ruling/src/test/expected/vitest/typescript-S4782.json +++ b/its/ruling/src/test/expected/vitest/typescript-S4782.json @@ -7,6 +7,9 @@ 78, 384 ], +"vitest:packages/pretty-format/src/types.ts": [ +108 +], "vitest:packages/runner/src/fixture.ts": [ 633 ], @@ -22,6 +25,9 @@ 238, 1266 ], +"vitest:packages/utils/src/diff/types.ts": [ +31 +], "vitest:packages/vitest/src/node/cli/filter.ts": [ 31 ], diff --git a/its/ruling/src/test/expected/vuetify/typescript-S4782.json b/its/ruling/src/test/expected/vuetify/typescript-S4782.json index 493afe45993..81763f6a314 100644 --- a/its/ruling/src/test/expected/vuetify/typescript-S4782.json +++ b/its/ruling/src/test/expected/vuetify/typescript-S4782.json @@ -1,10 +1,20 @@ { +"vuetify:packages/vuetify/src/composables/color.ts": [ +21, +21 +], "vuetify:packages/vuetify/src/composables/group.ts": [ 21, 22 ], +"vuetify:packages/vuetify/src/composables/rounded.ts": [ +11 +], "vuetify:packages/vuetify/src/composables/router.tsx": [ 42, 43 +], +"vuetify:packages/vuetify/src/framework.ts": [ +27 ] } diff --git a/packages/analysis/src/jsts/rules/S4782/rule.ts b/packages/analysis/src/jsts/rules/S4782/rule.ts index 0e2fbc81f19..2d684ce3ce0 100644 --- a/packages/analysis/src/jsts/rules/S4782/rule.ts +++ b/packages/analysis/src/jsts/rules/S4782/rule.ts @@ -20,9 +20,10 @@ import type { AST, Rule } from 'eslint'; import type estree from 'estree'; import type { TSESTree } from '@typescript-eslint/utils'; import { generateMeta } from '../helpers/generate-meta.js'; -import { isRequiredParserServices } from '../helpers/parser-services.js'; +import { isRequiredParserServices, RequiredParserServices } from '../helpers/parser-services.js'; import { report, toSecondaryLocation } from '../helpers/location.js'; import * as meta from './generated-meta.js'; +import ts from 'typescript'; export const rule: Rule.RuleModule = { meta: generateMeta(meta, { hasSuggestions: true }), @@ -45,8 +46,10 @@ export const rule: Rule.RuleModule = { if (!tsNode.optional || !optionalToken) { return; } - - const typeNode = getUndefinedTypeAnnotation(tsNode.typeAnnotation); + const typeNode = getUndefinedTypeAnnotation( + tsNode.typeAnnotation, + context.sourceCode.parserServices, + ); if (typeNode) { const suggest = getQuickFixSuggestions(context, optionalToken, typeNode); @@ -69,13 +72,38 @@ export const rule: Rule.RuleModule = { }, }; -function getUndefinedTypeAnnotation(tsTypeAnnotation?: TSESTree.TSTypeAnnotation) { - if (tsTypeAnnotation?.typeAnnotation.type === 'TSUnionType') { +function getUndefinedTypeAnnotation( + tsTypeAnnotation: TSESTree.TSTypeAnnotation | undefined, + services: RequiredParserServices, +) { + if (!tsTypeAnnotation) { + return undefined; + } + + if (tsTypeAnnotation.typeAnnotation.type === 'TSTypeReference') { + return getUndefinedFromTypeAlias(tsTypeAnnotation, services); + } + if (tsTypeAnnotation.typeAnnotation.type === 'TSUnionType') { return getUndefinedTypeNode(tsTypeAnnotation.typeAnnotation); } return undefined; } +function getUndefinedFromTypeAlias( + tsTypeAnnotation: TSESTree.TSTypeAnnotation, + services: RequiredParserServices, +): TSESTree.TypeNode | undefined { + const tsTypeNode = services.esTreeNodeToTSNodeMap.get(tsTypeAnnotation.typeAnnotation); + const checker: ts.TypeChecker = services.program.getTypeChecker(); + const type = checker.getTypeAtLocation(tsTypeNode); + if (!type.isUnion()) { + return undefined; + } + const hasUndefined = type.types.some(t => (t.flags & ts.TypeFlags.Undefined) !== 0); + const hasNonUndefined = type.types.some(t => (t.flags & ts.TypeFlags.Undefined) === 0); + return hasUndefined && hasNonUndefined ? tsTypeAnnotation.typeAnnotation : undefined; +} + function getUndefinedTypeNode(typeNode: TSESTree.TypeNode): TSESTree.TypeNode | undefined { if (typeNode.type === 'TSUndefinedKeyword') { return typeNode; diff --git a/packages/analysis/src/jsts/rules/S4782/unit.test.ts b/packages/analysis/src/jsts/rules/S4782/unit.test.ts index 94c8fe84afa..29ce231586b 100644 --- a/packages/analysis/src/jsts/rules/S4782/unit.test.ts +++ b/packages/analysis/src/jsts/rules/S4782/unit.test.ts @@ -53,6 +53,56 @@ describe('S4782', () => { specificAttribute?: undefined; };`, }, + { + code: ` + type StringOrNumber = string | number; + interface Example { + attribute?: StringOrNumber; + }; + type UndefinedAlias = undefined; + interface Example2 { + attribute?: UndefinedAlias; + }; + `, + }, + { + code: ` + type A = string; + type B = A; + type C = B; + interface Example { + attribute?: C; + }; + type D = undefined; + type E = D; + type F = E; + interface Example2 { + attribute?: F; + }; + `, + }, + { + code: ` + type Recursive = string | Recursive; + interface Example { + attribute?: Recursive; + }; + type RecursiveUndefined = undefined | Recursive; + interface Example2 { + attribute?: RecursiveUndefined; + };`, + }, + { + code: ` + type Box = T; + interface Example { + attribute?: Box; + }; + type BoxU = T; + interface Example2 { + attribute?: BoxU; + };`, + }, ], invalid: [ { @@ -286,6 +336,121 @@ describe('S4782', () => { }, ], }, + { + code: `type StringOrUndefined = string | undefined; + interface Example { + attribute?: StringOrUndefined; + };`, + errors: [ + { + message: + "Consider removing 'undefined' type or '?' specifier, one of them is redundant.", + suggestions: [ + { + desc: 'Remove "?" operator', + output: `type StringOrUndefined = string | undefined; + interface Example { + attribute: StringOrUndefined; + };`, + }, + ], + }, + ], + }, + { + code: ` + type NumberOrUndefined = number | undefined; + type Attribute = string | NumberOrUndefined; + interface Example { + attribute?: Attribute; + };`, + errors: [ + { + message: + "Consider removing 'undefined' type or '?' specifier, one of them is redundant.", + suggestions: [ + { + desc: 'Remove "?" operator', + output: ` + type NumberOrUndefined = number | undefined; + type Attribute = string | NumberOrUndefined; + interface Example { + attribute: Attribute; + };`, + }, + ], + }, + ], + }, + { + code: ` + type Maybe = T | undefined; + interface Example { + attribute?: Maybe; + };`, + errors: [ + { + message: + "Consider removing 'undefined' type or '?' specifier, one of them is redundant.", + suggestions: [ + { + desc: 'Remove "?" operator', + output: ` + type Maybe = T | undefined; + interface Example { + attribute: Maybe; + };`, + }, + ], + }, + ], + }, + { + code: ` + type Box = T; + interface Example { + attribute?: Box; + };`, + errors: [ + { + message: + "Consider removing 'undefined' type or '?' specifier, one of them is redundant.", + suggestions: [ + { + desc: 'Remove "?" operator', + output: ` + type Box = T; + interface Example { + attribute: Box; + };`, + }, + ], + }, + ], + }, + { + code: ` + type Wrapped = (string | undefined); + interface Example { + attribute?: Wrapped; + };`, + errors: [ + { + message: + "Consider removing 'undefined' type or '?' specifier, one of them is redundant.", + suggestions: [ + { + desc: 'Remove "?" operator', + output: ` + type Wrapped = (string | undefined); + interface Example { + attribute: Wrapped; + };`, + }, + ], + }, + ], + }, ], }, ); diff --git a/packages/analysis/tests/jsts/tools/testers/fixtures/tsconfig.json b/packages/analysis/tests/jsts/tools/testers/fixtures/tsconfig.json index 108dee06ed4..815616c1832 100644 --- a/packages/analysis/tests/jsts/tools/testers/fixtures/tsconfig.json +++ b/packages/analysis/tests/jsts/tools/testers/fixtures/tsconfig.json @@ -3,7 +3,7 @@ "allowJs": true, "noImplicitAny": true, "strict": false, - "strictNullChecks": false, + "strictNullChecks": true, "lib": ["ESNext", "DOM"], }, "include": [