diff --git a/eslint-plugin-expensify/boolean-conditional-rendering.js b/eslint-plugin-expensify/boolean-conditional-rendering.js new file mode 100644 index 0000000..113ed89 --- /dev/null +++ b/eslint-plugin-expensify/boolean-conditional-rendering.js @@ -0,0 +1,65 @@ +/* eslint-disable no-bitwise */ +const _ = require('underscore'); +const {ESLintUtils} = require('@typescript-eslint/utils'); +const ts = require('typescript'); + +module.exports = { + name: 'boolean-conditional-rendering', + meta: { + type: 'problem', + docs: { + description: 'Enforce boolean conditions in React conditional rendering', + recommended: 'error', + }, + schema: [], + messages: { + nonBooleanConditional: 'The left side of conditional rendering should be a boolean, not "{{type}}".', + }, + }, + defaultOptions: [], + create(context) { + function isJSXElement(node) { + return node.type === 'JSXElement' || node.type === 'JSXFragment'; + } + function isBoolean(type) { + return ( + (type.getFlags() + & (ts.TypeFlags.Boolean + | ts.TypeFlags.BooleanLike + | ts.TypeFlags.BooleanLiteral)) + !== 0 + || (type.isUnion() + && _.every( + type.types, + t => (t.getFlags() + & (ts.TypeFlags.Boolean + | ts.TypeFlags.BooleanLike + | ts.TypeFlags.BooleanLiteral)) + !== 0, + )) + ); + } + const parserServices = ESLintUtils.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + return { + LogicalExpression(node) { + if (!(node.operator === '&&' && isJSXElement(node.right))) { + return; + } + const leftType = typeChecker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node.left), + ); + if (!isBoolean(leftType)) { + const baseType = typeChecker.getBaseTypeOfLiteralType(leftType); + context.report({ + node: node.left, + messageId: 'nonBooleanConditional', + data: { + type: typeChecker.typeToString(baseType), + }, + }); + } + }, + }; + }, +}; diff --git a/eslint-plugin-expensify/tests/boolean-conditional-rendering.test.js b/eslint-plugin-expensify/tests/boolean-conditional-rendering.test.js new file mode 100644 index 0000000..3fd05f5 --- /dev/null +++ b/eslint-plugin-expensify/tests/boolean-conditional-rendering.test.js @@ -0,0 +1,260 @@ +const RuleTester = require('@typescript-eslint/rule-tester').RuleTester; +const rule = require('../boolean-conditional-rendering'); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + ecmaVersion: 2020, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run('boolean-conditional-rendering', rule, { + valid: [ + { + code: ` + const isActive = true; + isActive && ; + `, + }, + { + code: ` + const isActive = false; + isActive && ; + `, + }, + { + code: ` + const isVisible = Boolean(someValue); + isVisible && ; + `, + }, + { + code: ` + const user = { isLoggedIn: true, isBlocked: false }; + const isAuthorized = user.isLoggedIn && !user.isBlocked; + isAuthorized && ; + `, + }, + { + code: ` + function isAuthenticated() { return true; } + isAuthenticated() && ; + `, + }, + { + code: ` + const isReady: boolean = true; + isReady && ; + `, + }, + { + code: ` + const isNotActive = !isActive; + isNotActive && ; + `, + }, + { + code: ` + const condition = !!someValue; + condition && ; + `, + }, + { + code: ` + const condition = someValue as boolean; + condition && ; + `, + }, + { + code: ` + enum Status { Active, Inactive } + const isActive = status === Status.Active; + isActive && ; + `, + }, + { + code: ` + const isAvailable = checkAvailability(); + isAvailable && ; + function checkAvailability(): boolean { return true; } + `, + }, + ], + invalid: [ + { + code: ` + const condition = "string"; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'string'}, + }, + ], + }, + { + code: ` + const condition = 42; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'number'}, + }, + ], + }, + { + code: ` + const condition = []; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'any[]'}, + }, + ], + }, + { + code: ` + const condition = {}; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: '{}'}, + }, + ], + }, + { + code: ` + const condition = null; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'any'}, + }, + ], + }, + { + code: ` + const condition = undefined; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'any'}, + }, + ], + }, + { + code: ` + const condition = () => {}; + condition() && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'void'}, + }, + ], + }, + { + code: ` + const condition: unknown = someValue; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'unknown'}, + }, + ], + }, + { + code: ` + const condition: boolean | string = someValue; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'string | boolean'}, + }, + ], + }, + { + code: ` + const condition = someObject?.property; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'any'}, + }, + ], + }, + { + code: ` + enum Status { Active, Inactive } + const status = Status.Active; + status && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'string'}, + }, + ], + }, + { + code: ` + const condition = Promise.resolve(true); + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'Promise'}, + }, + ], + }, + { + code: ` + function getValue() { return "value"; } + getValue() && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'string'}, + }, + ], + }, + { + code: ` + const condition = someValue as string; + condition && ; + `, + errors: [ + { + messageId: 'nonBooleanConditional', + data: {type: 'string'}, + }, + ], + }, + ], +}); diff --git a/eslint-plugin-expensify/tests/react.tsx b/eslint-plugin-expensify/tests/react.tsx new file mode 100644 index 0000000..e69de29