Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/analysis/src/jsts/rules/S6478/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const REACT_INTL_COMPONENTS = new Set([
'$t', // common alias
]);

const REACT_TABLE_COLUMN_PROPERTIES = new Set(['accessor', 'columns', 'id']);

/**
* Checks if a JSXExpressionContainer is the `values` attribute of a react-intl component.
*/
Expand Down Expand Up @@ -120,6 +122,48 @@ function isReactIntlFormattingValue(node: estree.Node): boolean {
return false;
}

/**
* Checks if a function is used as a react-table column header renderer.
* react-table accepts `Header` functions in column definitions as render callbacks.
*/
function isReactTableColumnHeaderRenderer(node: estree.Node): boolean {
if (node.type !== 'ArrowFunctionExpression' && node.type !== 'FunctionExpression') {
return false;
}

const property = (node as TSESTree.Node).parent;
const columnDefinition = property?.parent;
if (
property?.type !== 'Property' ||
columnDefinition?.type !== 'ObjectExpression' ||
getStaticPropertyName(property) !== 'Header'
) {
return false;
}

return columnDefinition.properties.some(
columnProperty =>
columnProperty.type === 'Property' &&
REACT_TABLE_COLUMN_PROPERTIES.has(getStaticPropertyName(columnProperty) ?? ''),
);
}

function getStaticPropertyName(property: TSESTree.Property) {
if (property.computed) {
return undefined;
}

const { key } = property;
if (key.type === 'Identifier') {
return key.name;
}
if (key.type === 'Literal' && typeof key.value === 'string') {
return key.value;
}

return undefined;
}

export function decorate(rule: Rule.RuleModule): Rule.RuleModule {
return interceptReportForReact(
{
Expand All @@ -135,6 +179,11 @@ export function decorate(rule: Rule.RuleModule): Rule.RuleModule {
return;
}

// Skip react-table column header renderers.
if (isReactTableColumnHeaderRenderer(node)) {
return;
}

const message =
'Move this component definition out of the parent component and pass data as props.';
const loc = getMainNodeLocation(node, context);
Expand Down
72 changes: 72 additions & 0 deletions packages/analysis/src/jsts/rules/S6478/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,55 @@ describe('S6478', () => {
}
`,
},
{
code: `
function Parent() {
const columns = [
{
Header: () => <CustomHeader />,
accessor: 'name',
},
];
useTable({ columns });
return <Table columns={columns} />;
}
`,
},
{
code: `
function Parent() {
const columns = [
{
'Header': () => <CustomHeader />,
'accessor': 'name',
},
];
useTable({ columns });
return <Table columns={columns} />;
}
`,
},
{
code: `
function Parent() {
const columns = [
{
Header: function Header() {
return <CustomHeader />;
},
columns: [
{
Header: () => <NestedHeader />,
id: 'nested',
},
],
},
];
useTable({ columns });
return <Table columns={columns} />;
}
`,
},
],
invalid: [
{
Expand Down Expand Up @@ -128,6 +177,29 @@ describe('S6478', () => {
`,
errors: 1,
},
{
code: `
function Parent() {
const renderers = {
Header: () => <div />,
};
return <Something renderers={renderers} />;
}
`,
errors: 1,
},
{
code: `
function Parent() {
const renderers = {
[Header]: () => <div />,
accessor: 'name',
};
return <Something renderers={renderers} />;
}
`,
errors: 1,
},
],
});
});
Expand Down
Loading