]> source.dussan.org Git - sonarqube.git/commitdiff
[NO-JIRA] Custom eslint rule - no implicit coercion
authorViktor Vorona <viktor.vorona@sonarsource.com>
Wed, 19 Jul 2023 08:01:15 +0000 (10:01 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Jul 2023 20:03:06 +0000 (20:03 +0000)
server/sonar-web/.eslintrc
server/sonar-web/eslint-local-rules/__tests__/no-implicit-coersion-test.js [new file with mode: 0644]
server/sonar-web/eslint-local-rules/index.js
server/sonar-web/eslint-local-rules/no-implicit-coersion.js [new file with mode: 0644]

index eb6d15c4886230c69deadbfa7b5ef0efe70f32b5..fc3c8368b5e7cba4d14472aeec14fbee68cc1819 100644 (file)
@@ -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 (file)
index 0000000..fe5c346
--- /dev/null
@@ -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 (
+          <div>
+            {props.test !== undefined && <span>{props.test}</span>}
+            {props.test === undefined && <span>100</span>}
+          </div>
+        );
+      }`,
+    },
+  ],
+  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 (
+          <div>
+            {props.test && <span>{props.test}</span>}
+            {!props.test && <span>100</span>}
+          </div>
+        );
+      }`,
+      errors: [{ messageId: 'noImplicitCoersion' }, { messageId: 'noImplicitCoersion' }],
+    },
+  ],
+});
index 6f38c65a64578c8f7f791b2a29767868c330ac01..05b729dc2344441b31b887151b4d270f4f429022 100644 (file)
@@ -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 (file)
index 0000000..705b16a
--- /dev/null
@@ -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',
+    });
+  }
+}