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
JavaScript
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 };