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