Skip to content
Merged
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
85 changes: 63 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,37 @@ function getJSXElementName(jsx) {
}
}

function handleJSX(context, name, jsx) {
function handleJSX(context, name, exported, jsx) {
if (
!providerRegex.test(getJSXElementName(jsx)) &&
!jsx.openingElement.attributes.find(
(a) => a.name?.name === 'data-component',
)
) {
context.report({
node: jsx,
message: `${name} is missing the data-component attribute for the top-level element.`,
fix(fixer) {
return fixer.insertTextAfterRange(
jsx.openingElement.typeParameters
? jsx.openingElement.typeParameters.range
: jsx.openingElement.name.range,
` data-component="${name}"`,
);
context.possibleReports.push({
name,
exported,
report: {
node: jsx.openingElement,
message: `${name} is missing the data-component attribute for the top-level element.`,
fix(fixer) {
return fixer.insertTextAfterRange(
jsx.openingElement.typeParameters
? jsx.openingElement.typeParameters.range
: jsx.openingElement.name.range,
` data-component="${name}"`,
);
},
},
});
}
}

function handleBlockStatement(context, name, block) {
function handleBlockStatement(context, name, exported, block) {
// Find the root return statement. Are there any other types of returns we need to handle?
const ret = block.body.find((c) => c.type === 'ReturnStatement');
if (ret && ret.argument.type === 'JSXElement') {
handleJSX(context, name, ret.argument);
handleJSX(context, name, exported, ret.argument);
}
}

Expand All @@ -49,31 +53,31 @@ function isForwardRef(expression) {
return calleeName === 'forwardRef' && expression.arguments.length == 1;
}

function handleExpression(context, name, expression) {
function handleExpression(context, name, exported, expression) {
switch (expression.type) {
case 'FunctionExpression':
handleBlockStatement(context, name, expression.body);
handleBlockStatement(context, name, exported, expression.body);
break;
case 'ArrowFunctionExpression':
switch (expression.body.type) {
case 'JSXElement':
handleJSX(context, name, expression.body);
handleJSX(context, name, exported, expression.body);
break;
case 'BlockStatement':
handleBlockStatement(context, name, expression.body);
handleBlockStatement(context, name, exported, expression.body);
break;
}
break;
case 'ClassExpression':
expression.body.body.forEach((x) => {
if (x.type === 'MethodDefinition' && x.key.name === 'render') {
handleBlockStatement(context, name, x.value.body);
handleBlockStatement(context, name, exported, x.value.body);
}
});
break;
case 'CallExpression':
if (isForwardRef(expression)) {
handleExpression(context, name, expression.arguments[0]);
handleExpression(context, name, exported, expression.arguments[0]);
}
break;
}
Expand All @@ -94,6 +98,7 @@ const rules = {
create(context) {
return {
Program(root) {
context = { ...context, possibleReports: [] };
root.body.forEach((node) => {
// We will need to save any non-exported declarations and handle them only if they get exported at the end
let exported = false;
Expand All @@ -109,19 +114,34 @@ const rules = {
switch (node.type) {
case 'VariableDeclaration':
node.declarations.forEach((variable) => {
handleExpression(context, variable.id.name, variable.init);
handleExpression(
context,
variable.id.name,
exported,
variable.init,
);
});
break;
case 'FunctionDeclaration':
handleBlockStatement(context, node.id.name, node.body);
handleBlockStatement(
context,
node.id.name,
exported,
node.body,
);
break;
case 'ClassDeclaration':
node.body.body.forEach((x) => {
if (
x.type === 'MethodDefinition' &&
x.key.name === 'render'
) {
handleBlockStatement(context, node.id.name, x.value.body);
handleBlockStatement(
context,
node.id.name,
exported,
x.value.body,
);
return;
}
});
Expand All @@ -135,10 +155,31 @@ const rules = {
handleExpression(
context,
node.arguments[0].id.name,
exported,
node.arguments[0],
);
}
break;
case 'AssignmentExpression':
handleExpression(context, node.left.name, exported, node.right);
break;
case 'ExportNamedDeclaration':
node.specifiers.forEach((s) => {
const report = context.possibleReports.find(
(r) => r.name === s.local.name,
);
if (report) {
report.exported = true;
}
});
break;
}
});

// Report all issues for exported components
context.possibleReports.forEach((r) => {
if (r.exported) {
context.report(r.report);
}
});
},
Expand Down
58 changes: 39 additions & 19 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ const { join } = require('path');
// Test File Definitions
//------------------------------------------------------------------------------

const singleComponent = `const temp = () => {
const singleComponent = `export const temp = () => {
return <Icon data-component="temp" name="metric" size={24} />;
};`;

const singleComponentError = `const temp = () => {
const singleComponentError = `export const temp = () => {
return <Icon name="metric" size={24} />;
};`;

Expand All @@ -23,20 +23,20 @@ const defaultSingleComponentError = `export default function temp () {
};`;

const genericTest = `
const yAxis = (xScale, xTicks) => (
export const yAxis = (xScale, xTicks) => (
<BottomAxis<Date> data-component="yAxis" width={1} height={1} xScale={xScale} xTicks={xTicks}>
123
</BottomAxis>
); `;
const genericTestError = `
const yAxis = (xScale, xTicks) => (
export const yAxis = (xScale, xTicks) => (
<BottomAxis<Date> width={1} height={1} xScale={xScale} xTicks={xTicks}>
123
</BottomAxis>
); `;

const renamingComponentDoesntError = `
const myDiv = () => (
export const myDiv = () => (
<div data-component="temp"/>
); `;

Expand Down Expand Up @@ -83,33 +83,35 @@ export const FooChart: React.FC<FooChartProps> = props => {
const multipleComponentsErrors = `
const Component1 = () => <div />;
const Component2 = () => <span />;
export { Component1, Component2 }
`;
const multipleComponents = `
const Component1 = () => <div data-component="Component1" />;
const Component2 = () => <span data-component="Component2" />;
export { Component1, Component2 }
`;

const fragmentsWontUpdate = `const Component = () =>
const fragmentsWontUpdate = `export const Component = () =>
<>
<a/>
<a/>
<a/>
</>
;`;

const classComponent = `class Car extends React.Component {
const classComponent = `export class Car extends React.Component {
render() {
return <h2 data-component="Car">Hi, I am a Car!</h2>;
}
}`;

const classComponentError = `class Car extends React.Component {
const classComponentError = `export class Car extends React.Component {
render() {
return <h2>Hi, I am a Car!</h2>;
}
}`;

const classComponentNestedError = `class Car extends React.Component {
const classComponentNestedError = `export class Car extends React.Component {
render() {
const Door = () => (
<h1>I am a door!</h1>
Expand All @@ -123,7 +125,7 @@ const classComponentNestedError = `class Car extends React.Component {
}
}`;

const classComponentNested = `class Car extends React.Component {
const classComponentNested = `export class Car extends React.Component {
render() {
const Door = () => (
<h1>I am a door!</h1>
Expand Down Expand Up @@ -264,14 +266,22 @@ export const TernaryComponent = () => {
};
`;

const ifBlock = /* tsx */ `
export const IfBlockComponent = () => {
const active = useIsActive();
if (active) {
return <ActiveComponent />;
}
return <div />;
};
const exportedError = /* tsx */ `
const NotExported = () => <Foo />
export const Exported = () => <Foo />
export default DefaultExported = () => <Foo />
const ExportedAtEnd = () => <Foo />
const RenamedExport = () => <Foo />
export { ExportedAtEnd, RenamedExport as ThisIsRenamed }
`;

const exported = /* tsx */ `
const NotExported = () => <Foo />
export const Exported = () => <Foo data-component="Exported" />
export default DefaultExported = () => <Foo data-component="DefaultExported" />
const ExportedAtEnd = () => <Foo data-component="ExportedAtEnd" />
const RenamedExport = () => <Foo data-component="RenamedExport" />
export { ExportedAtEnd, RenamedExport as ThisIsRenamed }
`;

const tests = {
Expand Down Expand Up @@ -344,7 +354,7 @@ const tests = {
code: ternary,
},
{
code: ifBlock,
code: exported,
},
],
invalid: [
Expand Down Expand Up @@ -427,6 +437,16 @@ const tests = {
'InternalLink is missing the data-component attribute for the top-level element.',
],
},
{
code: exportedError,
output: exported,
errors: [
'Exported is missing the data-component attribute for the top-level element.',
'DefaultExported is missing the data-component attribute for the top-level element.',
'ExportedAtEnd is missing the data-component attribute for the top-level element.',
'RenamedExport is missing the data-component attribute for the top-level element.',
],
},
],
},
},
Expand Down