UNPKG

@react-analyzer/tsl

Version:

Bring the same linting functionality that https://eslint-react.xyz has to the TypeScript LSP.

287 lines (278 loc) 10 kB
import { defineRule, createRulesSet } from 'tsl'; import { compare } from 'compare-versions'; import ts, { SyntaxKind } from 'typescript'; import { getTsconfig } from 'get-tsconfig'; import { match, P, isMatching } from 'ts-pattern'; import { parseArgs } from 'util'; import { isTypeFlagSet, isFalseLiteralType, isTrueLiteralType } from 'ts-api-utils'; var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/eff/eff.ts var unit = void 0; function identity(x) { return x; } var dual = function(arity, body) { if (typeof arity === "function") { return function() { return arity(arguments) ? body.apply(this, arguments) : (self) => body(self, ...arguments); }; } switch (arity) { case 0: case 1: throw new RangeError(`Invalid arity ${arity}`); case 2: return function(a, b) { if (arguments.length >= 2) { return body(a, b); } return function(self) { return body(self, a); }; }; case 3: return function(a, b, c) { if (arguments.length >= 3) { return body(a, b, c); } return function(self) { return body(self, a, b); }; }; default: return function() { if (arguments.length >= arity) { return body.apply(this, arguments); } const args = arguments; return function(self) { return body(self, ...args); }; }; } }; dual(2, (ab, bc) => (a) => bc(ab(a))); // src/kit/Report.ts var Report_exports = {}; __export(Report_exports, { report: () => report, reportOrElse: () => reportOrElse }); var report = dual(2, (context, descriptor) => { if (descriptor == null) return; return context.report(descriptor); }); var reportOrElse = dual( 3, (context, descriptor, cb) => descriptor == null ? cb() : context.report(descriptor) ); // src/kit/CommandLine.ts var CommandLine_exports = {}; __export(CommandLine_exports, { getCommandLineOptions: () => getCommandLineOptions }); function getCommandLineOptions() { const { values } = parseArgs({ options: { project: { type: "string", short: "p" } } }); return values; } // src/analyzer/analyzer-options.ts var DEFAULT_ANALYZER_OPTIONS = { version: "19.1.0" }; function getAnalyzerOptions(context) { const { project = "tsconfig.json" } = CommandLine_exports.getCommandLineOptions(); const options = getTsconfig(project)?.config["react"]; return { ...DEFAULT_ANALYZER_OPTIONS, ...match(options).with({ version: P.string }, identity).otherwise(() => ({})) }; } var isAnyType = (type) => isTypeFlagSet(type, ts.TypeFlags.TypeParameter | ts.TypeFlags.Any); var isBigIntType = (type) => isTypeFlagSet(type, ts.TypeFlags.BigIntLike); var isBooleanType = (type) => isTypeFlagSet(type, ts.TypeFlags.BooleanLike); var isEnumType = (type) => isTypeFlagSet(type, ts.TypeFlags.EnumLike); var isFalsyBigIntType = (type) => type.isLiteral() && isMatching({ value: { base10Value: "0" } }, type); var isFalsyNumberType = (type) => type.isNumberLiteral() && type.value === 0; var isFalsyStringType = (type) => type.isStringLiteral() && type.value === ""; var isNeverType = (type) => isTypeFlagSet(type, ts.TypeFlags.Never); var isNullishType = (type) => isTypeFlagSet(type, ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike); var isNumberType = (type) => isTypeFlagSet(type, ts.TypeFlags.NumberLike); var isObjectType = (type) => !isTypeFlagSet( type, ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike | ts.TypeFlags.BooleanLike | ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike | ts.TypeFlags.TypeParameter | ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never ); var isStringType = (type) => isTypeFlagSet(type, ts.TypeFlags.StringLike); var isTruthyBigIntType = (type) => type.isLiteral() && isMatching({ value: { base10Value: P.not("0") } }, type); var isTruthyNumberType = (type) => type.isNumberLiteral() && type.value !== 0; var isTruthyStringType = (type) => type.isStringLiteral() && type.value !== ""; var isUnknownType = (type) => isTypeFlagSet(type, ts.TypeFlags.Unknown); function getVariantsOfTypes(types) { const variants = /* @__PURE__ */ new Set(); if (types.some(isUnknownType)) { variants.add("unknown"); return variants; } if (types.some(isNullishType)) { variants.add("nullish"); } const booleans = types.filter(isBooleanType); const boolean0 = booleans[0]; if (booleans.length === 1 && boolean0 != null) { if (isFalseLiteralType(boolean0)) { variants.add("falsy boolean"); } else if (isTrueLiteralType(boolean0)) { variants.add("truthy boolean"); } } else if (booleans.length === 2) { variants.add("boolean"); } const strings = types.filter(isStringType); if (strings.length > 0) { const evaluated = match(strings).when((types2) => types2.every(isTruthyStringType), () => "truthy string").when((types2) => types2.every(isFalsyStringType), () => "falsy string").otherwise(() => "string"); variants.add(evaluated); } const bigints = types.filter(isBigIntType); if (bigints.length > 0) { const evaluated = match(bigints).when((types2) => types2.every(isTruthyBigIntType), () => "truthy bigint").when((types2) => types2.every(isFalsyBigIntType), () => "falsy bigint").otherwise(() => "bigint"); variants.add(evaluated); } const numbers = types.filter(isNumberType); if (numbers.length > 0) { const evaluated = match(numbers).when((types2) => types2.every(isTruthyNumberType), () => "truthy number").when((types2) => types2.every(isFalsyNumberType), () => "falsy number").otherwise(() => "number"); variants.add(evaluated); } if (types.some(isEnumType)) { variants.add("enum"); } if (types.some(isObjectType)) { variants.add("object"); } if (types.some(isAnyType)) { variants.add("any"); } if (types.some(isNeverType)) { variants.add("never"); } return variants; } function isLogicalNegationExpression(node) { return node.kind === SyntaxKind.PrefixUnaryExpression && node.operator === SyntaxKind.ExclamationToken; } // src/rules/noLeakedConditionalRendering.ts var messages = { noLeakedConditionalRendering: (p) => `Potential leaked value ${p.value} that might cause unintentionally rendered values or rendering crashes.` }; var 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 = getVariantsOfTypes(ctx.utils.unionConstituents(leftType)); const isLeftTypeValid = Array.from(leftTypeVariants.values()).every((type) => allowedVariants.some((allowed) => allowed === type)); if (!isLeftTypeValid) { 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_exports.report(ctx, getReportDescriptor(node)); } } }; }); // src/index.ts var rules = createRulesSet({ /** * 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 */ noLeakedConditionalRendering // TODO: Port more rules from https://beta.eslint-react.xyz/docs/rules/overview }); export { rules };