@ospm/eslint-plugin-react-signals-hooks
Version:
ESLint plugin for React Signals hooks - enforces best practices, performance optimizations, and integration patterns for @preact/signals-react usage in React projects
834 lines • 43.4 kB
JavaScript
import { ESLintUtils, AST_NODE_TYPES, } from '@typescript-eslint/utils';
import { PerformanceOperations } from './utils/performance-constants.js';
import { endPhase, startPhase, recordMetric, startTracking, trackOperation, createPerformanceTracker, DEFAULT_PERFORMANCE_BUDGET, } from './utils/performance.js';
import { getRuleDocUrl } from './utils/urls.js';
function isJSXNode(node) {
return (node.type === AST_NODE_TYPES.JSXElement ||
node.type === AST_NODE_TYPES.JSXFragment ||
(node.type === AST_NODE_TYPES.ExpressionStatement &&
'expression' in node &&
'type' in node.expression &&
(node.expression.type === AST_NODE_TYPES.JSXElement ||
node.expression.type === AST_NODE_TYPES.JSXFragment)));
}
function getComplexity(node, visited = new Set()) {
if (visited.has(node)) {
return 0;
}
visited.add(node);
let complexity = 0;
if (isJSXNode(node)) {
complexity++;
}
else if ('type' in node && node.type === AST_NODE_TYPES.CallExpression) {
complexity++;
}
else if ('type' in node && node.type === AST_NODE_TYPES.ConditionalExpression) {
complexity += 2;
}
for (const key of [
'body',
'consequent',
'alternate',
'test',
'left',
'right',
'argument',
'callee',
'arguments',
'elements',
'properties',
]) {
const value = node[key];
if (typeof value !== 'undefined') {
if (Array.isArray(value)) {
for (const item of value) {
if (item && typeof item === 'object' && 'type' in item) {
complexity += getComplexity(item, visited);
}
}
}
else if (typeof value === 'object' && 'type' in value) {
complexity += getComplexity(value, visited);
}
}
}
visited.delete(node);
return complexity;
}
// Require at least one JSX branch (or effectively empty) to avoid firing for non-JSX ternaries (e.g., numbers)
function branchIsJsx(n) {
return n.type === AST_NODE_TYPES.JSXElement || n.type === AST_NODE_TYPES.JSXFragment;
}
function isEffectivelyEmpty(n, context) {
if (n.type === AST_NODE_TYPES.Literal) {
return (n.value === null ||
n.value === false ||
n.value === '' ||
n.raw === 'null' ||
n.raw === 'false' ||
n.raw === "''" ||
n.raw === '""');
}
if (n.type === AST_NODE_TYPES.Identifier) {
return n.name === 'undefined';
}
if (n.type === AST_NODE_TYPES.JSXFragment) {
const text = context.sourceCode.getText(n).replace(/\s+/g, '');
return text === '<></>';
}
return false;
}
let hasShowImport = false;
const signalVariables = new Set();
const ruleName = 'prefer-show-over-ternary';
function getSeverity(messageId, options) {
if (!options?.severity) {
return 'error';
}
switch (messageId) {
case 'preferShowOverTernary': {
return options.severity.preferShowOverTernary ?? 'error';
}
case 'suggestShowComponent': {
return options.severity.suggestShowComponent ?? 'error';
}
case 'addShowImport': {
return options.severity.addShowImport ?? 'error';
}
default: {
return 'error';
}
}
}
export const preferShowOverTernaryRule = ESLintUtils.RuleCreator((name) => {
return getRuleDocUrl(name);
})({
name: ruleName,
meta: {
type: 'suggestion',
fixable: 'code',
hasSuggestions: true,
docs: {
description: 'Prefer Show component over ternary for conditional rendering with signals',
url: getRuleDocUrl(ruleName),
},
messages: {
preferShowOverTernary: 'Prefer using the `<Show>` component instead of ternary for better performance with signal conditions.',
suggestShowComponent: 'Replace ternary with `<Show>` component',
addShowImport: 'Add `Show` import from @preact/signals-react/utils',
},
schema: [
{
type: 'object',
properties: {
minComplexity: {
type: 'number',
minimum: 1,
default: 2,
},
signalNames: {
type: 'array',
items: { type: 'string' },
default: ['signal', 'useSignal', 'createSignal'],
},
performance: {
type: 'object',
properties: {
maxTime: { type: 'number', minimum: 1 },
maxMemory: { type: 'number', minimum: 1 },
maxNodes: { type: 'number', minimum: 1 },
enableMetrics: { type: 'boolean' },
logMetrics: { type: 'boolean' },
maxOperations: {
type: 'object',
properties: Object.fromEntries(Object.entries(PerformanceOperations).map(([key]) => [
key,
{ type: 'number', minimum: 1 },
])),
},
},
additionalProperties: false,
},
severity: {
type: 'object',
properties: {
preferShowOverTernary: {
type: 'string',
enum: ['error', 'warn', 'off'],
},
suggestShowComponent: {
type: 'string',
enum: ['error', 'warn', 'off'],
},
addShowImport: {
type: 'string',
enum: ['error', 'warn', 'off'],
},
},
additionalProperties: false,
},
suffix: {
description: "Configurable suffix used to detect signal identifiers (default: 'Signal')",
type: 'string',
default: 'Signal',
},
},
additionalProperties: false,
},
],
},
defaultOptions: [
{
minComplexity: 2,
signalNames: ['signal', 'useSignal', 'createSignal'],
suffix: 'Signal',
performance: DEFAULT_PERFORMANCE_BUDGET,
},
],
create(context, [option]) {
const perfKey = `${ruleName}:${context.filename}${Date.now()}`;
startPhase(perfKey, 'ruleInit');
const perf = createPerformanceTracker(perfKey, option?.performance);
if (option?.performance?.enableMetrics === true) {
startTracking(context, perfKey, option.performance, ruleName);
}
if (option?.performance?.enableMetrics === true && option.performance.logMetrics === true) {
console.info(`${ruleName}: Initializing rule for file: ${context.filename}`);
console.info(`${ruleName}: Rule configuration:`, option);
}
recordMetric(perfKey, 'config', {
performance: {
enableMetrics: option?.performance?.enableMetrics,
logMetrics: option?.performance?.logMetrics,
},
});
let nodeCount = 0;
function shouldContinue() {
nodeCount++;
if (typeof option?.performance?.maxNodes === 'number' &&
nodeCount > option.performance.maxNodes) {
trackOperation(perfKey, PerformanceOperations.nodeBudgetExceeded);
return false;
}
return true;
}
trackOperation(perfKey, PerformanceOperations.ruleInit);
endPhase(perfKey, 'ruleInit');
// Where the Show component is exported from
const showModules = new Set(['@preact/signals-react/utils']);
function hasShowImportFromAny(context) {
return context.sourceCode.ast.body.some((node) => {
if (node.type !== AST_NODE_TYPES.ImportDeclaration) {
return false;
}
return (typeof node.source.value === 'string' &&
showModules.has(node.source.value) &&
node.specifiers.some((s) => {
return (s.type === AST_NODE_TYPES.ImportSpecifier &&
'name' in s.imported &&
s.imported.name === 'Show');
}));
});
}
function ensureShowImportAny(fixer, fixes, context) {
if (hasShowImportFromAny(context)) {
return;
}
const importDecls = context.sourceCode.ast.body.filter((n) => {
return n.type === AST_NODE_TYPES.ImportDeclaration;
});
// Prefer to merge into an existing Show module import, otherwise add a new import from utils
const anyShowImport = importDecls.find((d) => {
return typeof d.source.value === 'string' && showModules.has(d.source.value);
});
const importText = "import { Show } from '@preact/signals-react/utils';\n";
if (!anyShowImport) {
const lastImport = importDecls[importDecls.length - 1];
const firstStmt = context.sourceCode.ast.body[0];
if (typeof firstStmt === 'undefined') {
return;
}
fixes.push(typeof lastImport === 'undefined'
? fixer.insertTextBefore(firstStmt, importText)
: fixer.insertTextAfter(lastImport, `\n${importText.trimStart()}`));
return;
}
const hasNamespace = anyShowImport.specifiers.some((s) => {
return s.type === AST_NODE_TYPES.ImportNamespaceSpecifier;
});
if (anyShowImport.importKind === 'type' || hasNamespace) {
fixes.push(fixer.insertTextAfter(anyShowImport, `\n${importText}`));
return;
}
if (anyShowImport.specifiers.some((s) => {
return (s.type === AST_NODE_TYPES.ImportSpecifier &&
'name' in s.imported &&
s.imported.name === 'Show');
})) {
return;
}
const lastNamed = [...anyShowImport.specifiers]
.reverse()
.find((s) => {
return s.type === AST_NODE_TYPES.ImportSpecifier;
});
if (typeof lastNamed !== 'undefined') {
fixes.push(fixer.insertTextAfter(lastNamed, ', Show'));
return;
}
const defaultSpec = anyShowImport.specifiers.find((s) => {
return s.type === AST_NODE_TYPES.ImportDefaultSpecifier;
});
if (typeof defaultSpec !== 'undefined') {
fixes.push(fixer.replaceText(anyShowImport, `import ${defaultSpec.local.name}, { Show } from '${String(anyShowImport.source.value)}';`));
return;
}
fixes.push(fixer.insertTextAfter(anyShowImport, `\n${importText}`));
}
hasShowImport = hasShowImportFromAny(context);
startPhase(perfKey, 'ruleExecution');
return {
'*': (node) => {
if (!shouldContinue()) {
endPhase(perfKey, 'recordMetrics');
return;
}
perf.trackNode(node);
trackOperation(perfKey,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
PerformanceOperations[`${node.type}Processing`] ?? PerformanceOperations.nodeProcessing);
// Handle function scopes and top-level program scope to collect signal variables
if (node.type === AST_NODE_TYPES.FunctionDeclaration ||
node.type === AST_NODE_TYPES.FunctionExpression ||
node.type === AST_NODE_TYPES.ArrowFunctionExpression ||
node.type === AST_NODE_TYPES.Program) {
const scope = context.sourceCode.getScope(node);
for (const variable of scope.variables) {
if (variable.defs.some((def) => {
trackOperation(perfKey, PerformanceOperations.signalCheck);
return ('init' in def.node &&
def.node.init?.type === AST_NODE_TYPES.CallExpression &&
def.node.init.callee.type === AST_NODE_TYPES.Identifier &&
new Set(option?.signalNames ?? ['signal', 'useSignal', 'createSignal']).has(def.node.init.callee.name));
})) {
signalVariables.add(variable.name);
}
}
}
},
[AST_NODE_TYPES.VariableDeclarator](node) {
perf.trackNode(node);
if (node.id.type === AST_NODE_TYPES.Identifier &&
node.init &&
node.init.type === AST_NODE_TYPES.CallExpression &&
node.init.callee.type === AST_NODE_TYPES.Identifier &&
new Set(option?.signalNames ?? ['signal', 'useSignal', 'createSignal']).has(node.init.callee.name)) {
signalVariables.add(node.id.name);
}
},
[AST_NODE_TYPES.LogicalExpression](node) {
perf.trackNode(node);
if (!((node.parent !== null && // eslint-disable-line @typescript-eslint/no-unnecessary-condition
typeof node.parent !== 'undefined' &&
isJSXNode(node.parent)) ||
node.parent.type === AST_NODE_TYPES.JSXExpressionContainer) ||
!(node.left.type === AST_NODE_TYPES.Identifier && signalVariables.has(node.left.name)) ||
!(node.right.type === AST_NODE_TYPES.JSXElement ||
node.right.type === AST_NODE_TYPES.JSXFragment)) {
return;
}
if (getSeverity('preferShowOverTernary', option) === 'off') {
return;
}
const suggestions = [];
if (getSeverity('suggestShowComponent', option) !== 'off') {
suggestions.push({
messageId: 'suggestShowComponent',
*fix(fixer) {
const leftText = context.sourceCode.getText(node.left);
const rightText = context.sourceCode.getText(node.right);
// Determine mapping based on operator
function buildSingleLine(node) {
if (node.operator === '&&') {
return `<Show when={${leftText}}>${rightText}</Show>`;
}
// operator '||' -> render right when left is falsy
return `<Show when={!(${leftText})}>${rightText}</Show>`;
}
const baseIndent = ' '.repeat(context.sourceCode.getLocFromIndex(node.range[0]).column);
const indent1 = `${baseIndent} `;
const indent2 = `${baseIndent} `;
function buildMultiLine() {
if (node.operator === '&&') {
return [
`<Show`,
`${indent1}when={${leftText}}`,
`>`,
`${indent2}${rightText}`,
`</Show>`,
].join('\n');
}
return [
`<Show`,
`${indent1}when={!(${leftText})}`,
`>`,
`${indent2}${rightText}`,
`</Show>`,
].join('\n');
}
// If this logical expression is inside a JSXExpressionContainer, replace the container
// so we remove the surrounding braces `{}`.
const targetForReplace =
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
node.parent?.type === AST_NODE_TYPES.JSXExpressionContainer ? node.parent : node;
yield fixer.replaceText(targetForReplace, context.sourceCode.getText(node).includes('\n')
? buildMultiLine()
: buildSingleLine(node));
if (hasShowImport || hasShowImportFromAny(context)) {
return;
}
const fixes = [];
ensureShowImportAny(fixer, fixes, context);
for (const f of fixes) {
yield f;
}
},
});
}
if (!hasShowImportFromAny(context) && getSeverity('addShowImport', option) !== 'off') {
suggestions.push({
messageId: 'addShowImport',
fix(fixer) {
const fixes = [];
ensureShowImportAny(fixer, fixes, context);
return fixes.length > 0 ? fixes : null;
},
});
}
context.report({
node,
messageId: 'preferShowOverTernary',
suggest: suggestions,
});
},
[AST_NODE_TYPES.ReturnStatement](node) {
perf.trackNode(node);
if (node.argument === null ||
node.argument.type !== AST_NODE_TYPES.ConditionalExpression ||
!(branchIsJsx(node.argument.consequent) ||
branchIsJsx(node.argument.alternate) ||
isEffectivelyEmpty(node.argument.consequent, context) ||
isEffectivelyEmpty(node.argument.alternate, context))) {
return;
}
if (!(node.argument.test.type === AST_NODE_TYPES.Identifier &&
signalVariables.has(node.argument.test.name)) ||
!(typeof option?.minComplexity === 'number' &&
getComplexity(node.argument) >= option.minComplexity) ||
getSeverity('preferShowOverTernary', option) === 'off') {
return;
}
const suggestions = [];
if (getSeverity('suggestShowComponent', option) !== 'off') {
suggestions.push({
messageId: 'suggestShowComponent',
*fix(fixer) {
if (node.argument === null || !('consequent' in node.argument)) {
return;
}
const consequentText = context.sourceCode.getText(node.argument.consequent);
const childrenText = node.argument.consequent.type === AST_NODE_TYPES.JSXElement ||
node.argument.consequent.type === AST_NODE_TYPES.JSXFragment
? `${consequentText}`
: `{${consequentText}}`;
const alternateText = context.sourceCode.getText(node.argument.alternate);
const consEmpty = isEffectivelyEmpty(node.argument.consequent, context);
const altEmpty = isEffectivelyEmpty(node.argument.alternate, context);
if (consEmpty && altEmpty) {
return;
}
const baseIndent = ' '.repeat(context.sourceCode.getLocFromIndex(node.argument.range[0]).column);
const indent1 = `${baseIndent} `;
const indent2 = `${baseIndent} `;
function buildSingleLine(alternate) {
if (node.argument === null || !('test' in node.argument)) {
return '';
}
const testText = context.sourceCode.getText(node.argument.test);
if (consEmpty && !altEmpty) {
return `<Show when={!(${testText})}>${branchIsJsx(alternate) ? alternateText : `{${alternateText}}`}</Show>`;
}
else if (!consEmpty && altEmpty) {
return `<Show when={${testText}}>${childrenText}</Show>`;
}
return `<Show when={${testText}} fallback={${alternateText}}>${childrenText}</Show>`;
}
function buildMultiLine(alternate) {
if (node.argument === null || !('test' in node.argument)) {
return '';
}
const testText = context.sourceCode.getText(node.argument.test);
if (consEmpty && !altEmpty) {
return [
`<Show`,
`${indent1}when={!(${testText})}`,
`>`,
`${indent2}${branchIsJsx(alternate) ? alternateText : `{${alternateText}}`}`,
`</Show>`,
].join('\n');
}
if (!consEmpty && altEmpty) {
return [
`<Show`,
`${indent1}when={${testText}}`,
`>`,
`${indent2}${childrenText}`,
`</Show>`,
].join('\n');
}
return [
`<Show`,
`${indent1}when={${testText}}`,
`${indent1}fallback={${alternateText}}`,
`>`,
`${indent2}${childrenText}`,
`</Show>`,
].join('\n');
}
yield fixer.replaceText(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
node.argument.parent?.type === AST_NODE_TYPES.JSXExpressionContainer
? node.argument.parent
: node.argument, context.sourceCode.getText(node.argument).includes('\n')
? buildMultiLine(node.argument.alternate)
: buildSingleLine(node.argument.alternate));
if (hasShowImport) {
return;
}
if (hasShowImportFromAny(context)) {
return;
}
const fixes = [];
ensureShowImportAny(fixer, fixes, context);
for (const f of fixes) {
yield f;
}
},
});
}
if (!hasShowImportFromAny(context) && getSeverity('addShowImport', option) !== 'off') {
suggestions.push({
messageId: 'addShowImport',
fix(fixer) {
const fixes = [];
ensureShowImportAny(fixer, fixes, context);
return fixes.length > 0 ? fixes : null;
},
});
}
context.report({
node: node.argument,
messageId: 'preferShowOverTernary',
suggest: suggestions,
});
},
[AST_NODE_TYPES.Program](node) {
startPhase(perfKey, 'importAnalysis');
const hasJSX = node.body.some((n) => {
return isJSXNode(n);
});
if (!hasJSX) {
endPhase(perfKey, 'importAnalysis');
return;
}
hasShowImport = hasShowImportFromAny(context);
endPhase(perfKey, 'importAnalysis');
},
[AST_NODE_TYPES.ConditionalExpression](node) {
perf.trackNode(node);
// Do not report if this ternary is nested inside another ternary whose test is NOT a direct signal.
// Example to skip: condExpr ? null : directSignal ? A : B
// We only want the top-most conditional to be considered in such composite expressions.
if (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
node.parent?.type === AST_NODE_TYPES.ConditionalExpression &&
(node.parent.consequent === node || node.parent.alternate === node)) {
const parentTest = node.parent.test;
const parentIsDirectSignal = parentTest.type === AST_NODE_TYPES.Identifier && signalVariables.has(parentTest.name);
if (!parentIsDirectSignal) {
return;
}
}
if (!(((node.parent !== null && // eslint-disable-line @typescript-eslint/no-unnecessary-condition
typeof node.parent !== 'undefined' &&
isJSXNode(node.parent)) ||
node.parent.type === AST_NODE_TYPES.JSXExpressionContainer) &&
node.test.type === AST_NODE_TYPES.Identifier &&
signalVariables.has(node.test.name))) {
return;
}
if (!(branchIsJsx(node.consequent) ||
branchIsJsx(node.alternate) ||
isEffectivelyEmpty(node.consequent, context) ||
isEffectivelyEmpty(node.alternate, context))) {
return;
}
// Track conditional analysis
trackOperation(perfKey, PerformanceOperations.conditionalAnalysis);
const complexity = getComplexity(node);
trackOperation(perfKey, PerformanceOperations.complexityAnalysis);
if (!(typeof option?.minComplexity === 'number' && complexity >= option.minComplexity) ||
getSeverity('preferShowOverTernary', option) === 'off') {
return;
}
const suggestions = [];
if (getSeverity('suggestShowComponent', option) !== 'off') {
suggestions.push({
messageId: 'suggestShowComponent',
*fix(fixer) {
const consequentText = context.sourceCode.getText(node.consequent);
const childrenText = node.consequent.type === AST_NODE_TYPES.JSXElement ||
node.consequent.type === AST_NODE_TYPES.JSXFragment
? `${consequentText}`
: `{${consequentText}}`;
const alternateText = context.sourceCode.getText(node.alternate);
function isEffectivelyEmptyAlternate() {
if (node.alternate.type === AST_NODE_TYPES.Literal) {
// null, false, empty string
return (node.alternate.value === null ||
node.alternate.value === false ||
node.alternate.value === '' ||
node.alternate.raw === 'null' ||
node.alternate.raw === 'false' ||
node.alternate.raw === "''" ||
node.alternate.raw === '""');
}
if (node.alternate.type === AST_NODE_TYPES.Identifier) {
return node.alternate.name === 'undefined';
}
if (node.alternate.type === AST_NODE_TYPES.JSXFragment) {
return context.sourceCode.getText(node.alternate).replace(/\s+/g, '') === '<></>';
}
return false;
}
function isEffectivelyEmptyConsequent() {
if (node.consequent.type === AST_NODE_TYPES.Literal) {
// null, false, empty string
return (node.consequent.value === null ||
node.consequent.value === false ||
node.consequent.value === '' ||
node.consequent.raw === 'null' ||
node.consequent.raw === 'false' ||
node.consequent.raw === "''" ||
node.consequent.raw === '""');
}
if (node.consequent.type === AST_NODE_TYPES.Identifier) {
return node.consequent.name === 'undefined';
}
if (node.consequent.type === AST_NODE_TYPES.JSXFragment) {
return (context.sourceCode.getText(node.consequent).replace(/\s+/g, '') === '<></>');
}
return false;
}
const testText = context.sourceCode.getText(node.test);
const consEmpty = isEffectivelyEmptyConsequent();
const altEmpty = isEffectivelyEmptyAlternate();
// If both branches are effectively empty, skip providing a fix
if (consEmpty && altEmpty) {
return;
}
const baseIndent = ' '.repeat(context.sourceCode.getLocFromIndex(node.range[0]).column);
const indent1 = `${baseIndent} `;
const indent2 = `${baseIndent} `;
function buildSingleLine() {
if (consEmpty && !altEmpty) {
return `<Show when={!(${testText})}>${node.alternate.type === AST_NODE_TYPES.JSXElement ||
node.alternate.type === AST_NODE_TYPES.JSXFragment
? alternateText
: `{${alternateText}}`}</Show>`;
}
else if (!consEmpty && altEmpty) {
return `<Show when={${testText}}>${childrenText}</Show>`;
}
return `<Show when={${testText}} fallback={${alternateText}}>${childrenText}</Show>`;
}
function buildMultiLine() {
if (consEmpty && !altEmpty) {
return [
`<Show`,
`${indent1}when={!(${testText})}`,
`>`,
`${indent2}${node.alternate.type === AST_NODE_TYPES.JSXElement ||
node.alternate.type === AST_NODE_TYPES.JSXFragment
? alternateText
: `{${alternateText}}`}`,
`</Show>`,
].join('\n');
}
if (!consEmpty && altEmpty) {
return [
`<Show`,
`${indent1}when={${testText}}`,
`>`,
`${indent2}${childrenText}`,
`</Show>`,
].join('\n');
}
// Both have content: include fallback on its own line
return [
`<Show`,
`${indent1}when={${testText}}`,
`${indent1}fallback={${alternateText}}`,
`>`,
`${indent2}${childrenText}`,
`</Show>`,
].join('\n');
}
yield fixer.replaceText(
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
node.parent?.type === AST_NODE_TYPES.JSXExpressionContainer ? node.parent : node, context.sourceCode.getText(node).includes('\n')
? buildMultiLine()
: buildSingleLine());
if (hasShowImport || hasShowImportFromAny(context)) {
return;
}
const fixes = [];
ensureShowImportAny(fixer, fixes, context);
for (const f of fixes) {
yield f;
}
},
});
}
if (!hasShowImport && getSeverity('addShowImport', option) !== 'off') {
suggestions.push({
messageId: 'addShowImport',
fix(fixer) {
const fixes = [];
ensureShowImportAny(fixer, fixes, context);
return fixes.length > 0 ? fixes : null;
},
});
}
context.report({
node,
messageId: 'preferShowOverTernary',
suggest: suggestions,
fix(fixer) {
const consequentText = context.sourceCode.getText(node.consequent);
const childrenText = node.consequent.type === AST_NODE_TYPES.JSXElement ||
node.consequent.type === AST_NODE_TYPES.JSXFragment
? `${consequentText}`
: `{${consequentText}}`;
const alternateText = context.sourceCode.getText(node.alternate);
function isEffectivelyEmptyAlternate() {
const alt = node.alternate;
if (alt.type === AST_NODE_TYPES.Literal) {
return (alt.value === null ||
alt.value === false ||
alt.value === '' ||
alt.raw === 'null' ||
alt.raw === 'false' ||
alt.raw === "''" ||
alt.raw === '""');
}
if (alt.type === AST_NODE_TYPES.Identifier) {
return alt.name === 'undefined';
}
if (alt.type === AST_NODE_TYPES.JSXFragment) {
return context.sourceCode.getText(alt).replace(/\s+/g, '') === '<></>';
}
return false;
}
function isEffectivelyEmptyConsequent() {
if (node.consequent.type === AST_NODE_TYPES.Literal) {
return (node.consequent.value === null ||
node.consequent.value === false ||
node.consequent.value === '' ||
node.consequent.raw === 'null' ||
node.consequent.raw === 'false' ||
node.consequent.raw === "''" ||
node.consequent.raw === '""');
}
if (node.consequent.type === AST_NODE_TYPES.Identifier) {
return node.consequent.name === 'undefined';
}
if (node.consequent.type === AST_NODE_TYPES.JSXFragment) {
return context.sourceCode.getText(node.consequent).replace(/\s+/g, '') === '<></>';
}
return false;
}
const testText = context.sourceCode.getText(node.test);
const consEmpty = isEffectivelyEmptyConsequent();
const altEmpty = isEffectivelyEmptyAlternate();
if (consEmpty && altEmpty) {
return null;
}
const baseIndent = ' '.repeat(context.sourceCode.getLocFromIndex(node.range[0]).column);
const indent1 = `${baseIndent} `;
const indent2 = `${baseIndent} `;
function buildSingleLine() {
if (consEmpty && !altEmpty) {
return `<Show when={!(${testText})}>${node.alternate.type === AST_NODE_TYPES.JSXElement ||
node.alternate.type === AST_NODE_TYPES.JSXFragment
? alternateText
: `{${alternateText}}`}</Show>`;
}
else if (!consEmpty && altEmpty) {
return `<Show when={${testText}}>${childrenText}</Show>`;
}
return `<Show when={${testText}} fallback={${alternateText}}>${childrenText}</Show>`;
}
function buildMultiLine() {
if (consEmpty && !altEmpty) {
return [
`<Show`,
`${indent1}when={!(${testText})}`,
`>`,
`${indent2}${node.alternate.type === AST_NODE_TYPES.JSXElement ||
node.alternate.type === AST_NODE_TYPES.JSXFragment
? alternateText
: `{${alternateText}}`}`,
`</Show>`,
].join('\n');
}
if (!consEmpty && altEmpty) {
return [
`<Show`,
`${indent1}when={${testText}}`,
`>`,
`${indent2}${childrenText}`,
`</Show>`,
].join('\n');
}
return [
`<Show`,
`${indent1}when={${testText}}`,
`${indent1}fallback={${alternateText}}`,
`>`,
`${indent2}${childrenText}`,
`</Show>`,
].join('\n');
}
const fixes = [];
fixes.push(fixer.replaceText(node, context.sourceCode.getText(node).includes('\n')
? buildMultiLine()
: buildSingleLine()));
if (!hasShowImport && !hasShowImportFromAny(context)) {
ensureShowImportAny(fixer, fixes, context);
}
return fixes;
},
});
},
[`${AST_NODE_TYPES.Program}:exit`](node) {
startPhase(perfKey, 'programExit');
perf.trackNode(node);
perf['Program:exit']();
endPhase(perfKey, 'programExit');
},
};
},
});
//# sourceMappingURL=prefer-show-over-ternary.js.map