Skip to content

Commit

Permalink
Merge pull request #121 from rayane-djouah/add-eslint-rule-for-valida…
Browse files Browse the repository at this point in the history
…ting-left-side-condition

boolean-conditional-rendering rule
  • Loading branch information
mountiny authored Oct 17, 2024
2 parents 0902fa7 + 7ae1666 commit 17c4d7f
Show file tree
Hide file tree
Showing 3 changed files with 325 additions and 0 deletions.
65 changes: 65 additions & 0 deletions eslint-plugin-expensify/boolean-conditional-rendering.js
Original file line number Diff line number Diff line change
@@ -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),
},
});
}
},
};
},
};
260 changes: 260 additions & 0 deletions eslint-plugin-expensify/tests/boolean-conditional-rendering.test.js
Original file line number Diff line number Diff line change
@@ -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 && <MyComponent />;
`,
},
{
code: `
const isActive = false;
isActive && <MyComponent />;
`,
},
{
code: `
const isVisible = Boolean(someValue);
isVisible && <MyComponent />;
`,
},
{
code: `
const user = { isLoggedIn: true, isBlocked: false };
const isAuthorized = user.isLoggedIn && !user.isBlocked;
isAuthorized && <MyComponent />;
`,
},
{
code: `
function isAuthenticated() { return true; }
isAuthenticated() && <MyComponent />;
`,
},
{
code: `
const isReady: boolean = true;
isReady && <ReadyComponent />;
`,
},
{
code: `
const isNotActive = !isActive;
isNotActive && <MyComponent />;
`,
},
{
code: `
const condition = !!someValue;
condition && <MyComponent />;
`,
},
{
code: `
const condition = someValue as boolean;
condition && <MyComponent />;
`,
},
{
code: `
enum Status { Active, Inactive }
const isActive = status === Status.Active;
isActive && <MyComponent />;
`,
},
{
code: `
const isAvailable = checkAvailability();
isAvailable && <MyComponent />;
function checkAvailability(): boolean { return true; }
`,
},
],
invalid: [
{
code: `
const condition = "string";
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'string'},
},
],
},
{
code: `
const condition = 42;
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'number'},
},
],
},
{
code: `
const condition = [];
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'any[]'},
},
],
},
{
code: `
const condition = {};
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: '{}'},
},
],
},
{
code: `
const condition = null;
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'any'},
},
],
},
{
code: `
const condition = undefined;
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'any'},
},
],
},
{
code: `
const condition = () => {};
condition() && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'void'},
},
],
},
{
code: `
const condition: unknown = someValue;
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'unknown'},
},
],
},
{
code: `
const condition: boolean | string = someValue;
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'string | boolean'},
},
],
},
{
code: `
const condition = someObject?.property;
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'any'},
},
],
},
{
code: `
enum Status { Active, Inactive }
const status = Status.Active;
status && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'string'},
},
],
},
{
code: `
const condition = Promise.resolve(true);
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'Promise<boolean>'},
},
],
},
{
code: `
function getValue() { return "value"; }
getValue() && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'string'},
},
],
},
{
code: `
const condition = someValue as string;
condition && <MyComponent />;
`,
errors: [
{
messageId: 'nonBooleanConditional',
data: {type: 'string'},
},
],
},
],
});
Empty file.

0 comments on commit 17c4d7f

Please sign in to comment.