import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.js"; const message = "Use Predicate.isNotNull/isNotUndefined/isNotNullish from effect instead of hand-rolled nullish predicates."; const nullishOperators = new Set(["!==", "!=", "===", "=="]); const isNullLiteral = (node) => node?.type === "Literal" && node.value === null;const isUndefinedIdentifier = (node) => isIdentifier(node, "undefined");const isNullishLiteral = (node) => isNullLiteral(node) || isUndefinedIdentifier(node); const isNullishComparison = (node, identifierName) => { const expression = unwrapExpression(node); if (expression?.type !== "BinaryExpression" || !nullishOperators.has(expression.operator)) { return false; } const left = unwrapExpression(expression.left); const right = unwrapExpression(expression.right); return ( (isIdentifier(left, identifierName) && isNullishLiteral(right)) || (isIdentifier(right, identifierName) && isNullishLiteral(left)) );}; const getSingleIdentifierParamName = (params) => { if (params.length !== 1) return undefined; const param = unwrapExpression(params[0]); return param?.type === "Identifier" ? param.name : undefined;}; const isNullishPredicateFunction = (node) => { const name = getSingleIdentifierParamName(node.params ?? []); return name !== undefined && isNullishComparison(node.body, name);}; const isFilterCall = (node) => { const callee = unwrapExpression(node.callee); return callee?.type === "MemberExpression" && getPropertyName(callee.property) === "filter";}; const importsEffect = (node) => { const source = node.source; return source?.type === "Literal" && source.value === "effect";}; export default { meta: { type: "problem", docs: { description: message, }, }, create(context) { let hasEffectImport = false; return { ImportDeclaration(node) { if (importsEffect(node)) { hasEffectImport = true; } }, VariableDeclarator(node) { if (!hasEffectImport) return; const init = unwrapExpression(node.init); if (init?.type === "ArrowFunctionExpression" && isNullishPredicateFunction(init)) { context.report({ node: init, message }); } }, FunctionDeclaration(node) { if (!hasEffectImport) return; if (isNullishPredicateFunction(node)) { context.report({ node, message }); } }, CallExpression(node) { if (!hasEffectImport || !isFilterCall(node)) return; const predicate = unwrapExpression(node.arguments[0]); if ( (predicate?.type === "ArrowFunctionExpression" || predicate?.type === "FunctionExpression") && isNullishPredicateFunction(predicate) ) { context.report({ node: predicate, message }); } }, }; },};
import { getPropertyName, isIdentifier, unwrapExpression } from "../utils.js";
const message =
"Use Predicate.isNotNull/isNotUndefined/isNotNullish from effect instead of hand-rolled nullish predicates.";
const nullishOperators = new Set(["!==", "!=", "===", "=="]);
const isNullLiteral = (node) => node?.type === "Literal" && node.value === null;
const isUndefinedIdentifier = (node) => isIdentifier(node, "undefined");
const isNullishLiteral = (node) => isNullLiteral(node) || isUndefinedIdentifier(node);
const isNullishComparison = (node, identifierName) => {
const expression = unwrapExpression(node);
if (expression?.type !== "BinaryExpression" || !nullishOperators.has(expression.operator)) {
return false;
}
const left = unwrapExpression(expression.left);
const right = unwrapExpression(expression.right);
return (
(isIdentifier(left, identifierName) && isNullishLiteral(right)) ||
(isIdentifier(right, identifierName) && isNullishLiteral(left))
);
};
const getSingleIdentifierParamName = (params) => {
if (params.length !== 1) return undefined;
const param = unwrapExpression(params[0]);
return param?.type === "Identifier" ? param.name : undefined;
};
const isNullishPredicateFunction = (node) => {
const name = getSingleIdentifierParamName(node.params ?? []);
return name !== undefined && isNullishComparison(node.body, name);
};
const isFilterCall = (node) => {
const callee = unwrapExpression(node.callee);
return callee?.type === "MemberExpression" && getPropertyName(callee.property) === "filter";
};
const importsEffect = (node) => {
const source = node.source;
return source?.type === "Literal" && source.value === "effect";
};
export default {
meta: {
type: "problem",
docs: {
description: message,
},
},
create(context) {
let hasEffectImport = false;
return {
ImportDeclaration(node) {
if (importsEffect(node)) {
hasEffectImport = true;
}
},
VariableDeclarator(node) {
if (!hasEffectImport) return;
const init = unwrapExpression(node.init);
if (init?.type === "ArrowFunctionExpression" && isNullishPredicateFunction(init)) {
context.report({ node: init, message });
}
},
FunctionDeclaration(node) {
if (!hasEffectImport) return;
if (isNullishPredicateFunction(node)) {
context.report({ node, message });
}
},
CallExpression(node) {
if (!hasEffectImport || !isFilterCall(node)) return;
const predicate = unwrapExpression(node.arguments[0]);
if (
(predicate?.type === "ArrowFunctionExpression" ||
predicate?.type === "FunctionExpression") &&
isNullishPredicateFunction(predicate)
) {
context.report({ node: predicate, message });
}
},
};
},
};