diff --git a/its/ruling/src/test/expected/jsts/ant-design/typescript-S6606.json b/its/ruling/src/test/expected/jsts/ant-design/typescript-S6606.json index 74c5c754059..03507f30923 100644 --- a/its/ruling/src/test/expected/jsts/ant-design/typescript-S6606.json +++ b/its/ruling/src/test/expected/jsts/ant-design/typescript-S6606.json @@ -2,9 +2,6 @@ "ant-design:components/_util/statusUtils.tsx": [ 23 ], -"ant-design:components/affix/index.tsx": [ -80 -], "ant-design:components/alert/ErrorBoundary.tsx": [ 34 ], @@ -16,8 +13,7 @@ 149 ], "ant-design:components/cascader/index.tsx": [ -217, -221 +217 ], "ant-design:components/descriptions/index.tsx": [ 38, @@ -35,17 +31,9 @@ 86, 87 ], -"ant-design:components/input/Input.tsx": [ -93 -], "ant-design:components/list/index.tsx": [ -194, -221, 266 ], -"ant-design:components/locale-provider/LocaleReceiver.tsx": [ -59 -], "ant-design:components/menu/SubMenu.tsx": [ 78 ], @@ -72,77 +60,25 @@ 25 ], "ant-design:components/progress/progress.tsx": [ -81, -91 -], -"ant-design:components/radio/radio.tsx": [ -53 +81 ], "ant-design:components/space/index.tsx": [ 37 ], -"ant-design:components/spin/index.tsx": [ -113 -], -"ant-design:components/statistic/utils.tsx": [ -40 -], "ant-design:components/table/Table.tsx": [ -148, -174, -282 -], -"ant-design:components/table/hooks/useFilter/FilterDropdown.tsx": [ -161, -182, -240, -256, -269, -282, -395, -410 -], -"ant-design:components/table/hooks/useFilter/index.tsx": [ -134, -149 +174 ], "ant-design:components/table/hooks/usePagination.ts": [ 81 ], -"ant-design:components/table/hooks/useSelection.tsx": [ -63, -99, -115, -115, -172 -], -"ant-design:components/table/hooks/useSorter.tsx": [ -144 -], "ant-design:components/tabs/index.tsx": [ 65 ], -"ant-design:components/transfer/ListItem.tsx": [ -37, -58, -78 -], -"ant-design:components/transfer/index.tsx": [ -133, -379 -], -"ant-design:components/transfer/operation.tsx": [ -38, -48 -], "ant-design:components/tree-select/index.tsx": [ -159, -162 +159 ], "ant-design:components/typography/Base/index.tsx": [ -207, -244, -246 +207 ], "ant-design:components/upload/UploadList/ListItem.tsx": [ 104, @@ -150,21 +86,14 @@ 205 ], "ant-design:components/upload/utils.tsx": [ -48, 57, 57 ], "ant-design:site/theme/template/Home/DesignPage/InteractiveIcon.tsx": [ 38 ], -"ant-design:site/theme/template/Layout/Header/algolia-config.ts": [ -6 -], "ant-design:site/theme/template/utils.tsx": [ 31, -31, -61, -78, -91 +31 ] } diff --git a/its/ruling/src/test/expected/jsts/courselit/typescript-S6606.json b/its/ruling/src/test/expected/jsts/courselit/typescript-S6606.json index cf4fff8f291..e3b4b73048c 100644 --- a/its/ruling/src/test/expected/jsts/courselit/typescript-S6606.json +++ b/its/ruling/src/test/expected/jsts/courselit/typescript-S6606.json @@ -4,7 +4,6 @@ ], "courselit:apps/web/lib/graphql.ts": [ 103, -105, 106 ] } diff --git a/its/ruling/src/test/expected/jsts/desktop/typescript-S6606.json b/its/ruling/src/test/expected/jsts/desktop/typescript-S6606.json index 46f78c73a51..035f49acdba 100644 --- a/its/ruling/src/test/expected/jsts/desktop/typescript-S6606.json +++ b/its/ruling/src/test/expected/jsts/desktop/typescript-S6606.json @@ -1,19 +1,14 @@ { -"desktop:app/src/cli/commands/help.ts": [ -59 -], "desktop:app/src/highlighter/index.ts": [ 512, 513 ], "desktop:app/src/lib/actions-log-parser/action-log-parser.ts": [ -97, -519 +97 ], "desktop:app/src/lib/api.ts": [ 1484, -1667, -1789 +1667 ], "desktop:app/src/lib/branch.ts": [ 42 @@ -21,12 +16,6 @@ "desktop:app/src/lib/ci-checks/ci-checks.ts": [ 656 ], -"desktop:app/src/lib/editors/lookup.ts": [ -58 -], -"desktop:app/src/lib/find-account.ts": [ -62 -], "desktop:app/src/lib/format-commit-message.ts": [ 31 ], @@ -53,23 +42,10 @@ "desktop:app/src/lib/git/rebase.ts": [ 572 ], -"desktop:app/src/lib/git/spawn.ts": [ -125 -], -"desktop:app/src/lib/git/stash.ts": [ -106, -169 -], "desktop:app/src/lib/git/status.ts": [ 104, 117 ], -"desktop:app/src/lib/helpers/pull-request-matching.ts": [ -25 -], -"desktop:app/src/lib/highlighter/worker.ts": [ -44 -], "desktop:app/src/lib/http.ts": [ 68, 69 @@ -78,14 +54,10 @@ 130, 158 ], -"desktop:app/src/lib/menu-update.ts": [ -43 -], "desktop:app/src/lib/source-map-support.ts": [ 127 ], "desktop:app/src/lib/stats/stats-store.ts": [ -418, 541, 542, 1921 @@ -95,12 +67,6 @@ ], "desktop:app/src/lib/stores/app-store.ts": [ 1200, -2095, -2104, -2671, -2684, -3123, -3303, 3961, 4867, 4897, @@ -109,27 +75,13 @@ 6613, 6644 ], -"desktop:app/src/lib/stores/cloning-repositories-store.ts": [ -68 -], "desktop:app/src/lib/stores/git-store.ts": [ -477, -864, -1080, -1090, -1171, -1242 +1090 ], "desktop:app/src/lib/stores/github-user-store.ts": [ 67, 108 ], -"desktop:app/src/lib/stores/helpers/find-default-remote.ts": [ -15 -], -"desktop:app/src/lib/stores/pull-request-coordinator.ts": [ -250 -], "desktop:app/src/lib/stores/repositories-store.ts": [ 374 ], @@ -154,44 +106,19 @@ "desktop:app/src/ui/app-menu/app-menu.tsx": [ 290 ], -"desktop:app/src/ui/app.tsx": [ -812 -], -"desktop:app/src/ui/autocompletion/user-autocompletion-provider.tsx": [ -59 -], -"desktop:app/src/ui/branches/branch-list.tsx": [ -144 -], "desktop:app/src/ui/branches/pull-request-list-item.tsx": [ 150 ], -"desktop:app/src/ui/branches/pull-request-list.tsx": [ -343 -], -"desktop:app/src/ui/check-runs/ci-check-run-list.tsx": [ -124 -], -"desktop:app/src/ui/clone-repository/cloneable-repository-filter-list.tsx": [ -111 -], "desktop:app/src/ui/create-tag/create-tag-dialog.tsx": [ 46 ], "desktop:app/src/ui/dialog/dialog.tsx": [ -368, -388, -388, -388 +368 ], "desktop:app/src/ui/diff/diff-explorer.ts": [ -41, 176, 194 ], -"desktop:app/src/ui/dispatcher/error-handlers.ts": [ -83 -], "desktop:app/src/ui/lib/author-input.tsx": [ 376 ], @@ -219,12 +146,6 @@ "desktop:app/src/ui/lib/ref-name-text-box.tsx": [ 63 ], -"desktop:app/src/ui/lib/toggle-button.tsx": [ -47 -], -"desktop:app/src/ui/lib/update-store.ts": [ -89 -], "desktop:app/src/ui/main-process-proxy.ts": [ 303 ], @@ -238,9 +159,7 @@ "desktop:app/src/ui/repositories-list/group-repositories.ts": [ 82, 90, -92, 139, -153, 160 ], "desktop:app/src/ui/repository-settings/repository-settings.tsx": [ diff --git a/its/ruling/src/test/expected/jsts/eigen/typescript-S6606.json b/its/ruling/src/test/expected/jsts/eigen/typescript-S6606.json index 6696294125b..0dc56d80f54 100644 --- a/its/ruling/src/test/expected/jsts/eigen/typescript-S6606.json +++ b/its/ruling/src/test/expected/jsts/eigen/typescript-S6606.json @@ -2,10 +2,6 @@ "eigen:dangerfile.ts": [ 131 ], -"eigen:src/app/Components/ArtworkFilter/Filters/YearOptions.tsx": [ -66, -67 -], "eigen:src/app/Components/ArtworkGrids/ArtworkGridItem.tests.tsx": [ 263 ], @@ -28,15 +24,6 @@ "eigen:src/app/Scenes/Artwork/Components/PartnerCard.tsx": [ 117 ], -"eigen:src/app/Scenes/City/Components/AllEvents.tsx": [ -77 -], -"eigen:src/app/Scenes/MyCollection/Screens/Artwork/Components/MyCollectionArtworkHeader.tsx": [ -58 -], -"eigen:src/app/Scenes/Sale/Components/SaleLotsList.tsx": [ -96 -], "eigen:src/app/Scenes/SellWithArtsy/SubmitArtwork/ArtworkDetails/utils/createOrUpdateSubmission.ts": [ 41 ], diff --git a/its/ruling/src/test/expected/jsts/postgraphql/typescript-S6606.json b/its/ruling/src/test/expected/jsts/postgraphql/typescript-S6606.json index a99ba3f647d..9a81666e270 100644 --- a/its/ruling/src/test/expected/jsts/postgraphql/typescript-S6606.json +++ b/its/ruling/src/test/expected/jsts/postgraphql/typescript-S6606.json @@ -3,35 +3,7 @@ 42 ], "postgraphql:src/graphql/schema/createGqlSchema.ts": [ -36, -37, -38, -40, -41 -], -"postgraphql:src/graphql/schema/createMutationGqlField.ts": [ -76 -], -"postgraphql:src/graphql/schema/createMutationPayloadGqlType.ts": [ -35 -], -"postgraphql:src/postgraphql/postgraphql.ts": [ -58, -82 -], -"postgraphql:src/postgraphql/schema/createPostGraphQLSchema.ts": [ -51, -137 -], -"postgraphql:src/postgres/inventory/addPgCatalogToInventory.ts": [ -18 -], -"postgraphql:src/postgres/inventory/collection/PgCollectionKey.ts": [ -200 -], -"postgraphql:src/postgres/inventory/paginator/PgCollectionPaginator.ts": [ -106, -111 +36 ], "postgraphql:src/postgres/inventory/paginator/PgPaginatorOrderingAttributes.ts": [ 34 diff --git a/its/ruling/src/test/expected/jsts/react-cloud-music/typescript-S6606.json b/its/ruling/src/test/expected/jsts/react-cloud-music/typescript-S6606.json deleted file mode 100644 index 2c4043e5c15..00000000000 --- a/its/ruling/src/test/expected/jsts/react-cloud-music/typescript-S6606.json +++ /dev/null @@ -1,5 +0,0 @@ -{ -"react-cloud-music:ts-music/src/store/index.ts": [ -9 -] -} diff --git a/its/ruling/src/test/expected/jsts/vuetify/typescript-S6606.json b/its/ruling/src/test/expected/jsts/vuetify/typescript-S6606.json index 4dbc2d83f3a..ce5f93093eb 100644 --- a/its/ruling/src/test/expected/jsts/vuetify/typescript-S6606.json +++ b/its/ruling/src/test/expected/jsts/vuetify/typescript-S6606.json @@ -1,14 +1,7 @@ { -"vuetify:packages/api-generator/src/utils.ts": [ -5, -6 -], "vuetify:packages/docs/src/components/app/Markup.vue": [ 101 ], -"vuetify:packages/docs/src/components/examples/Example.vue": [ -122 -], "vuetify:packages/docs/src/composables/codepen.ts": [ 63, 67 @@ -27,11 +20,6 @@ "vuetify:packages/vuetify/src/components/VOtpInput/VOtpInput.ts": [ 199 ], -"vuetify:packages/vuetify/src/components/VOverlay/useActivator.tsx": [ -91, -96, -111 -], "vuetify:packages/vuetify/src/components/VSkeletonLoader/VSkeletonLoader.ts": [ 126, 191 @@ -43,7 +31,6 @@ 60 ], "vuetify:packages/vuetify/src/components/VTimePicker/VTimePicker.ts": [ -221, 286 ], "vuetify:packages/vuetify/src/components/VTimePicker/VTimePickerClock.ts": [ @@ -52,13 +39,6 @@ "vuetify:packages/vuetify/src/composables/group.ts": [ 332 ], -"vuetify:packages/vuetify/src/composables/mutationObserver.ts": [ -22 -], -"vuetify:packages/vuetify/src/composables/nested/selectStrategies.ts": [ -50, -178 -], "vuetify:packages/vuetify/src/composables/proxiedModel.ts": [ 24 ], @@ -68,12 +48,6 @@ "vuetify:packages/vuetify/src/composables/validation.ts": [ 115 ], -"vuetify:packages/vuetify/src/directives/ripple/index.ts": [ -184 -], -"vuetify:packages/vuetify/src/services/goto/index.ts": [ -24 -], "vuetify:packages/vuetify/src/util/getCurrentInstance.ts": [ 10 ] diff --git a/packages/jsts/src/rules/S6606/fixtures/index.ts b/packages/jsts/src/rules/S6606/fixtures/index.ts new file mode 100644 index 00000000000..36db4933d90 --- /dev/null +++ b/packages/jsts/src/rules/S6606/fixtures/index.ts @@ -0,0 +1 @@ +// intentionally left empty diff --git a/packages/jsts/src/rules/S6606/fixtures/tsconfig.json b/packages/jsts/src/rules/S6606/fixtures/tsconfig.json new file mode 100644 index 00000000000..9647394afeb --- /dev/null +++ b/packages/jsts/src/rules/S6606/fixtures/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "strictNullChecks": true + } +} diff --git a/packages/jsts/src/rules/S6606/index.ts b/packages/jsts/src/rules/S6606/index.ts new file mode 100644 index 00000000000..8938f0a8fd1 --- /dev/null +++ b/packages/jsts/src/rules/S6606/index.ts @@ -0,0 +1,20 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export * from './rule'; diff --git a/packages/jsts/src/rules/S6606/rule.ts b/packages/jsts/src/rules/S6606/rule.ts new file mode 100644 index 00000000000..b4c28e7620a --- /dev/null +++ b/packages/jsts/src/rules/S6606/rule.ts @@ -0,0 +1,60 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { tsEslintRules } from '../typescript-eslint'; +import { type Rule } from 'eslint'; +import { + getTypeFromTreeNode, + interceptReport, + isNullOrUndefinedType, + isObjectType, + isBooleanType, +} from '../helpers'; +import { type LogicalExpression } from 'estree'; + +const preferNullishCoalescingRule = tsEslintRules['prefer-nullish-coalescing']; + +export const rule = interceptReport(preferNullishCoalescingRule, (context, reportDescriptor) => { + const { node: token, messageId } = reportDescriptor as Rule.ReportDescriptor & { + node: Rule.Node; + messageId: string; + }; + + if (messageId === 'preferNullishOverOr') { + const services = context.sourceCode.parserServices; + const rangeIndex = ( + token as { + range: [number, number]; + } + ).range[0]; + const node = context.sourceCode.getNodeByRangeIndex(rangeIndex) as LogicalExpression; + const leftOperand = node.left; + const leftOperandType = getTypeFromTreeNode(leftOperand, services); + + if ( + leftOperandType.isUnion() && + leftOperandType.types.some(isNullOrUndefinedType) && + (leftOperandType.types.some(isBooleanType) || leftOperandType.types.some(isObjectType)) + ) { + return; + } + } + + context.report(reportDescriptor); +}); diff --git a/packages/jsts/src/rules/S6606/unit.test.ts b/packages/jsts/src/rules/S6606/unit.test.ts new file mode 100644 index 00000000000..10412c4ce1c --- /dev/null +++ b/packages/jsts/src/rules/S6606/unit.test.ts @@ -0,0 +1,106 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { rule } from './rule'; +import { RuleTester } from 'eslint'; +import path from 'path'; + +const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + project: `./tsconfig.json`, + tsconfigRootDir: path.join(__dirname, 'fixtures'), + }, +}); + +ruleTester.run('S6606', rule, { + valid: [ + { + code: ` + function foo(value: string) { + return value || 'default'; + } + `, + filename: path.join(__dirname, 'fixtures/index.ts'), + }, + { + code: ` + function foo(value: string | number) { + return value || 'default'; + } + `, + filename: path.join(__dirname, 'fixtures/index.ts'), + }, + { + code: ` + function foo(value: boolean | null) { + return value || 'default'; + } + `, + filename: path.join(__dirname, 'fixtures/index.ts'), + }, + { + code: ` + function foo(value: { baz: number } | null) { + return value || 'default'; + } + `, + filename: path.join(__dirname, 'fixtures/index.ts'), + }, + { + code: ` + function foo(value: Date | null) { + return value || 'default'; + } + `, + filename: path.join(__dirname, 'fixtures/index.ts'), + }, + ], + invalid: [ + { + code: ` + function foo(value: string | null) { + return value || 'default'; + } + `, + filename: path.join(__dirname, 'fixtures/index.ts'), + errors: 1, + }, + { + code: ` + function foo(value: number | null) { + return value || 'default'; + } + `, + filename: path.join(__dirname, 'fixtures/index.ts'), + errors: 1, + }, + { + code: ` + function foo(value: bigint | null) { + return value || 'default'; + } + `, + filename: path.join(__dirname, 'fixtures/index.ts'), + errors: 1, + }, + ], +}); diff --git a/packages/jsts/src/rules/helpers/type.ts b/packages/jsts/src/rules/helpers/type.ts index 05fea7df43a..a31dd34e1fe 100644 --- a/packages/jsts/src/rules/helpers/type.ts +++ b/packages/jsts/src/rules/helpers/type.ts @@ -291,3 +291,15 @@ export function isBooleanLiteralType(type: ts.Type): type is ts.Type & { export function isBooleanTrueType(type: ts.Type) { return isBooleanLiteralType(type) && type.intrinsicName === 'true'; } + +export function isBooleanType({ flags }: ts.Type) { + return flags & ts.TypeFlags.BooleanLike; +} + +export function isNullOrUndefinedType({ flags }: ts.Type) { + return flags & ts.TypeFlags.Null || flags & ts.TypeFlags.Undefined; +} + +export function isObjectType({ flags }: ts.Type) { + return flags & ts.TypeFlags.Object; +} diff --git a/packages/jsts/src/rules/index.ts b/packages/jsts/src/rules/index.ts index b68e938c33a..38148d4aab3 100644 --- a/packages/jsts/src/rules/index.ts +++ b/packages/jsts/src/rules/index.ts @@ -232,6 +232,7 @@ import { rule as S6572 } from './S6572'; // prefer-enum-initializers import { rule as S4138 } from './S4138'; // prefer-for-of import { rule as S6598 } from './S6598'; // prefer-function-type import { rule as S4156 } from './S4156'; // prefer-namespace-keyword +import { rule as S6606 } from './S6606'; // prefer-nullish-coalescing import { rule as S6661 } from './S6661'; // prefer-object-spread import { rule as S4634 } from './S4634'; // prefer-promise-shorthand import { rule as S6666 } from './S6666'; // prefer-spread @@ -521,6 +522,7 @@ rules['prefer-enum-initializers'] = S6572; rules['prefer-for-of'] = S4138; rules['prefer-function-type'] = S6598; rules['prefer-namespace-keyword'] = S4156; +rules['prefer-nullish-coalescing'] = S6606; rules['prefer-object-spread'] = S6661; rules['prefer-promise-shorthand'] = S4634; rules['prefer-spread'] = S6666;