From 2c34b53123edd9edb1a660fb3dbee010e73f27d3 Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Wed, 19 Jul 2023 10:01:15 +0200 Subject: [PATCH] [NO-JIRA] Custom eslint rule - no implicit coercion --- server/sonar-web/.eslintrc | 3 +- .../__tests__/no-implicit-coersion-test.js | 234 ++++++++++++++++++ server/sonar-web/eslint-local-rules/index.js | 1 + .../no-implicit-coersion.js | 79 ++++++ 4 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 server/sonar-web/eslint-local-rules/__tests__/no-implicit-coersion-test.js create mode 100644 server/sonar-web/eslint-local-rules/no-implicit-coersion.js diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc index eb6d15c4886..fc3c8368b5e 100644 --- a/server/sonar-web/.eslintrc +++ b/server/sonar-web/.eslintrc @@ -15,6 +15,7 @@ "local-rules/convert-class-to-function-component": "warn", "local-rules/no-conditional-rendering-of-deferredspinner": "warn", "local-rules/use-jest-mocked": "warn", - "local-rules/use-await-expect-async-matcher": "warn" + "local-rules/use-await-expect-async-matcher": "warn", + "local-rules/no-implicit-coersion": "warn" } } diff --git a/server/sonar-web/eslint-local-rules/__tests__/no-implicit-coersion-test.js b/server/sonar-web/eslint-local-rules/__tests__/no-implicit-coersion-test.js new file mode 100644 index 00000000000..fe5c346f1ac --- /dev/null +++ b/server/sonar-web/eslint-local-rules/__tests__/no-implicit-coersion-test.js @@ -0,0 +1,234 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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. + */ +const { RuleTester } = require('eslint'); +const noImplicitCoersion = require('../no-implicit-coersion'); + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + parser: require.resolve('@typescript-eslint/parser'), +}); + +ruleTester.run('no-implicit-coersion', noImplicitCoersion, { + valid: [ + { + code: ` + function test(value?: number) { + if (value === undefined) { + return true; + } + }`, + }, + { + code: ` + function test(value?: number) { + if (Boolean(value)) { + return true; + } + }`, + }, + { + code: ` + function test(value?: {}) { + if (!value) { + return true; + } + }`, + }, + { + code: ` + function test(value: string) { + if (value !== '') { + return true; + } + }`, + }, + { + code: ` + function test(value?: number | {}) { + return value !== undefined && value.toString(); + }`, + }, + { + code: ` + function test(value?: number) { + return value ?? 100; + }`, + }, + { + code: ` + interface Props { + test?: number; + } + function Test(props: Props) { + if (props.test !== undefined) { + return props.test * 10; + } + return 100; + }`, + }, + { + code: ` + interface Props { + test?: number; + check: boolean + } + function Test(props: Props) { + if (props.check && props.test !== undefined) { + return props.test * 10; + } + return 100; + }`, + }, + { + code: ` + interface Props { + test?: React.ReactNode; + } + function Test(props: Props) { + if (props.test) { + return props.test; + } + return null; + }`, + }, + { + code: ` + interface Props { + test?: number; + } + function Test(props: Props) { + return ( +
+ {props.test !== undefined && {props.test}} + {props.test === undefined && 100} +
+ ); + }`, + }, + ], + invalid: [ + { + code: ` + function test(value?: number) { + if (!value) { + return true; + } + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + function test(value?: number) { + if (value) { + return true; + } + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + function test(value: string) { + if (value) { + return true; + } + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + function test(value?: number) { + return value && value > -1; + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + function test(value?: string | {}) { + if (value) { + return 1; + } + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + function test(value?: number) { + return value || 100; + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + interface Props { + test?: number | {}; + } + function Test(props: Props) { + return props.test && props.test.toString(); + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + interface Props { + test?: number; + } + function Test(props: Props) { + if (props.test) { + return props.test * 10; + } + return 100; + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + interface Props { + test?: number; + check: boolean + } + function Test(props: Props) { + if (props.check && props.test) { + return props.test * 10; + } + return 100; + }`, + errors: [{ messageId: 'noImplicitCoersion' }], + }, + { + code: ` + interface Props { + test?: number; + } + function Test(props: Props) { + return ( +
+ {props.test && {props.test}} + {!props.test && 100} +
+ ); + }`, + errors: [{ messageId: 'noImplicitCoersion' }, { messageId: 'noImplicitCoersion' }], + }, + ], +}); diff --git a/server/sonar-web/eslint-local-rules/index.js b/server/sonar-web/eslint-local-rules/index.js index 6f38c65a645..05b729dc234 100644 --- a/server/sonar-web/eslint-local-rules/index.js +++ b/server/sonar-web/eslint-local-rules/index.js @@ -26,4 +26,5 @@ module.exports = { 'use-metrickey-enum': require('./use-metrickey-enum'), 'use-metrictype-enum': require('./use-metrictype-enum'), 'use-await-expect-async-matcher': require('./use-await-expect-async-matcher'), + 'no-implicit-coersion': require('./no-implicit-coersion'), }; diff --git a/server/sonar-web/eslint-local-rules/no-implicit-coersion.js b/server/sonar-web/eslint-local-rules/no-implicit-coersion.js new file mode 100644 index 00000000000..705b16a90f0 --- /dev/null +++ b/server/sonar-web/eslint-local-rules/no-implicit-coersion.js @@ -0,0 +1,79 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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. + */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'Enforce using explicit comparison instead of implicit coercion for certain variable types', + category: 'Best Practices', + recommended: true, + }, + messages: { + noImplicitCoersion: + 'Use explicit comparison instead of implicit coercion for strings and numbers.', + }, + }, + create(context) { + return { + UnaryExpression: (node) => { + const { argument, operator } = node; + + if (operator === '!') { + checkImplicitCoercion(context, argument); + } + }, + LogicalExpression: (node) => { + const { left, operator } = node; + if (operator === '??') { + return; + } + if (isVariableOrObjectField(left)) { + checkImplicitCoercion(context, left); + } + }, + IfStatement: (node) => { + const { test } = node; + checkImplicitCoercion(context, test); + }, + }; + }, +}; + +const isForbiddenType = (type) => + type.intrinsicName === 'number' || type.intrinsicName === 'string'; + +const isVariableOrObjectField = (node) => + node.type === 'Identifier' || node.type === 'MemberExpression'; + +function checkImplicitCoercion(context, argument) { + const tsNodeMap = context.parserServices.esTreeNodeToTSNodeMap; + const typeChecker = context.parserServices?.program?.getTypeChecker(); + const type = typeChecker.getTypeAtLocation(tsNodeMap.get(argument)); + if (type.aliasSymbol && type.aliasSymbol.name === 'ReactNode') { + return; + } + if (type.isUnion() ? type.types.some(isForbiddenType) : isForbiddenType(type)) { + context.report({ + node: argument, + messageId: 'noImplicitCoersion', + }); + } +} -- 2.39.5