UNPKG

tsl-react

Version:

A unified plugin that combines all individual plugins from the react-analyzer monorepo into one.

117 lines (115 loc) 3.63 kB
import { compare } from "compare-versions"; import { defineRule } from "tsl"; import { SyntaxKind } from "typescript"; import { isLogicalNegationExpression } from "@react-analyzer/ast"; import * as RA from "@react-analyzer/core"; import { unit } from "@react-analyzer/eff"; import { report } from "@react-analyzer/kit"; import { getAnalyzerOptions } from "@react-analyzer/shared"; //#region src/rules/no-leaked-conditional-rendering.ts /** @internal */ const messages = { noLeakedConditionalRendering: (p) => `Potential leaked value ${p.value} that might cause unintentionally rendered values or rendering crashes.` }; /** * Prevents problematic leaked values from being rendered. * * Using the && operator to render some element conditionally in JSX can cause unexpected values being rendered, or even crashing the rendering. * * **Examples** * * ```tsx * import React from "react"; * * interface MyComponentProps { * count: number; * } * * function MyComponent({ count }: MyComponentProps) { * return <div>{count && <span>There are {count} results</span>}</div>; * // ^^^^^ * // - Potential leaked value 'count' that might cause unintentionally rendered values or rendering crashes. * } * ``` * * ```tsx * import React from "react"; * * interface MyComponentProps { * items: string[]; * } * * function MyComponent({ items }: MyComponentProps) { * return <div>{items.length && <List items={items} />}</div>; * // ^^^^^^^^^^^^ * // - Potential leaked value 'items.length' that might cause unintentionally rendered values or rendering crashes. * } * ``` * * ```tsx * import React from "react"; * * interface MyComponentProps { * items: string[]; * } * * function MyComponent({ items }: MyComponentProps) { * return <div>{items[0] && <List items={items} />}</div>; * // ^^^^^^^^ * // - Potential leaked value 'items[0]' that might cause unintentionally rendered values or rendering crashes. * } * ``` * * @since 0.0.0 */ const noLeakedConditionalRendering = defineRule(() => { return { name: "@react-analyzer/noLeakedConditionalRendering", createData(ctx) { const { version } = getAnalyzerOptions(); const state = { isWithinJsxExpression: false }; const allowedVariants = [ "any", "boolean", "nullish", "object", "falsy boolean", "truthy bigint", "truthy boolean", "truthy number", "truthy string", ...compare(version, "18.0.0", "<") ? [] : ["string", "falsy string"] ]; function getReportDescriptor(node) { if (isLogicalNegationExpression(node.left)) return unit; const leftType = ctx.utils.getConstrainedTypeAtLocation(node.left); const leftTypeVariants = RA.getVariantsOfTypes(ctx.utils.unionConstituents(leftType)); if (!Array.from(leftTypeVariants.values()).every((type) => allowedVariants.some((allowed) => allowed === type))) return { node: node.left, message: messages.noLeakedConditionalRendering({ value: node.left.getText() }) }; return unit; } return { state, version, allowedVariants, getReportDescriptor }; }, visitor: { JsxExpression(ctx) { ctx.data.state.isWithinJsxExpression = true; }, JsxExpression_exit(ctx) { ctx.data.state.isWithinJsxExpression = false; }, BinaryExpression(ctx, node) { const { state, getReportDescriptor } = ctx.data; if (!state.isWithinJsxExpression) return; if (node.operatorToken.kind !== SyntaxKind.AmpersandAmpersandToken) return; report(ctx, getReportDescriptor(node)); } } }; }); //#endregion export { noLeakedConditionalRendering };