Skip to content

Commit ef2d828

Browse files
author
Andy Miller
authored
Fire rule only for exported components (#9)
* Only highlight jsx opening node for issues * Fire rule only for exported components
1 parent b1c2460 commit ef2d828

File tree

2 files changed

+102
-41
lines changed

2 files changed

+102
-41
lines changed

index.js

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,37 @@ function getJSXElementName(jsx) {
1111
}
1212
}
1313

14-
function handleJSX(context, name, jsx) {
14+
function handleJSX(context, name, exported, jsx) {
1515
if (
1616
!providerRegex.test(getJSXElementName(jsx)) &&
1717
!jsx.openingElement.attributes.find(
1818
(a) => a.name?.name === 'data-component',
1919
)
2020
) {
21-
context.report({
22-
node: jsx,
23-
message: `${name} is missing the data-component attribute for the top-level element.`,
24-
fix(fixer) {
25-
return fixer.insertTextAfterRange(
26-
jsx.openingElement.typeParameters
27-
? jsx.openingElement.typeParameters.range
28-
: jsx.openingElement.name.range,
29-
` data-component="${name}"`,
30-
);
21+
context.possibleReports.push({
22+
name,
23+
exported,
24+
report: {
25+
node: jsx.openingElement,
26+
message: `${name} is missing the data-component attribute for the top-level element.`,
27+
fix(fixer) {
28+
return fixer.insertTextAfterRange(
29+
jsx.openingElement.typeParameters
30+
? jsx.openingElement.typeParameters.range
31+
: jsx.openingElement.name.range,
32+
` data-component="${name}"`,
33+
);
34+
},
3135
},
3236
});
3337
}
3438
}
3539

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

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

52-
function handleExpression(context, name, expression) {
56+
function handleExpression(context, name, exported, expression) {
5357
switch (expression.type) {
5458
case 'FunctionExpression':
55-
handleBlockStatement(context, name, expression.body);
59+
handleBlockStatement(context, name, exported, expression.body);
5660
break;
5761
case 'ArrowFunctionExpression':
5862
switch (expression.body.type) {
5963
case 'JSXElement':
60-
handleJSX(context, name, expression.body);
64+
handleJSX(context, name, exported, expression.body);
6165
break;
6266
case 'BlockStatement':
63-
handleBlockStatement(context, name, expression.body);
67+
handleBlockStatement(context, name, exported, expression.body);
6468
break;
6569
}
6670
break;
6771
case 'ClassExpression':
6872
expression.body.body.forEach((x) => {
6973
if (x.type === 'MethodDefinition' && x.key.name === 'render') {
70-
handleBlockStatement(context, name, x.value.body);
74+
handleBlockStatement(context, name, exported, x.value.body);
7175
}
7276
});
7377
break;
7478
case 'CallExpression':
7579
if (isForwardRef(expression)) {
76-
handleExpression(context, name, expression.arguments[0]);
80+
handleExpression(context, name, exported, expression.arguments[0]);
7781
}
7882
break;
7983
}
@@ -94,6 +98,7 @@ const rules = {
9498
create(context) {
9599
return {
96100
Program(root) {
101+
context = { ...context, possibleReports: [] };
97102
root.body.forEach((node) => {
98103
// We will need to save any non-exported declarations and handle them only if they get exported at the end
99104
let exported = false;
@@ -109,19 +114,34 @@ const rules = {
109114
switch (node.type) {
110115
case 'VariableDeclaration':
111116
node.declarations.forEach((variable) => {
112-
handleExpression(context, variable.id.name, variable.init);
117+
handleExpression(
118+
context,
119+
variable.id.name,
120+
exported,
121+
variable.init,
122+
);
113123
});
114124
break;
115125
case 'FunctionDeclaration':
116-
handleBlockStatement(context, node.id.name, node.body);
126+
handleBlockStatement(
127+
context,
128+
node.id.name,
129+
exported,
130+
node.body,
131+
);
117132
break;
118133
case 'ClassDeclaration':
119134
node.body.body.forEach((x) => {
120135
if (
121136
x.type === 'MethodDefinition' &&
122137
x.key.name === 'render'
123138
) {
124-
handleBlockStatement(context, node.id.name, x.value.body);
139+
handleBlockStatement(
140+
context,
141+
node.id.name,
142+
exported,
143+
x.value.body,
144+
);
125145
return;
126146
}
127147
});
@@ -135,10 +155,31 @@ const rules = {
135155
handleExpression(
136156
context,
137157
node.arguments[0].id.name,
158+
exported,
138159
node.arguments[0],
139160
);
140161
}
141162
break;
163+
case 'AssignmentExpression':
164+
handleExpression(context, node.left.name, exported, node.right);
165+
break;
166+
case 'ExportNamedDeclaration':
167+
node.specifiers.forEach((s) => {
168+
const report = context.possibleReports.find(
169+
(r) => r.name === s.local.name,
170+
);
171+
if (report) {
172+
report.exported = true;
173+
}
174+
});
175+
break;
176+
}
177+
});
178+
179+
// Report all issues for exported components
180+
context.possibleReports.forEach((r) => {
181+
if (r.exported) {
182+
context.report(r.report);
142183
}
143184
});
144185
},

test.js

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ const { join } = require('path');
66
// Test File Definitions
77
//------------------------------------------------------------------------------
88

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

13-
const singleComponentError = `const temp = () => {
13+
const singleComponentError = `export const temp = () => {
1414
return <Icon name="metric" size={24} />;
1515
};`;
1616

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

2525
const genericTest = `
26-
const yAxis = (xScale, xTicks) => (
26+
export const yAxis = (xScale, xTicks) => (
2727
<BottomAxis<Date> data-component="yAxis" width={1} height={1} xScale={xScale} xTicks={xTicks}>
2828
123
2929
</BottomAxis>
3030
); `;
3131
const genericTestError = `
32-
const yAxis = (xScale, xTicks) => (
32+
export const yAxis = (xScale, xTicks) => (
3333
<BottomAxis<Date> width={1} height={1} xScale={xScale} xTicks={xTicks}>
3434
123
3535
</BottomAxis>
3636
); `;
3737

3838
const renamingComponentDoesntError = `
39-
const myDiv = () => (
39+
export const myDiv = () => (
4040
<div data-component="temp"/>
4141
); `;
4242

@@ -83,33 +83,35 @@ export const FooChart: React.FC<FooChartProps> = props => {
8383
const multipleComponentsErrors = `
8484
const Component1 = () => <div />;
8585
const Component2 = () => <span />;
86+
export { Component1, Component2 }
8687
`;
8788
const multipleComponents = `
8889
const Component1 = () => <div data-component="Component1" />;
8990
const Component2 = () => <span data-component="Component2" />;
91+
export { Component1, Component2 }
9092
`;
9193

92-
const fragmentsWontUpdate = `const Component = () =>
94+
const fragmentsWontUpdate = `export const Component = () =>
9395
<>
9496
<a/>
9597
<a/>
9698
<a/>
9799
</>
98100
;`;
99101

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

106-
const classComponentError = `class Car extends React.Component {
108+
const classComponentError = `export class Car extends React.Component {
107109
render() {
108110
return <h2>Hi, I am a Car!</h2>;
109111
}
110112
}`;
111113

112-
const classComponentNestedError = `class Car extends React.Component {
114+
const classComponentNestedError = `export class Car extends React.Component {
113115
render() {
114116
const Door = () => (
115117
<h1>I am a door!</h1>
@@ -123,7 +125,7 @@ const classComponentNestedError = `class Car extends React.Component {
123125
}
124126
}`;
125127

126-
const classComponentNested = `class Car extends React.Component {
128+
const classComponentNested = `export class Car extends React.Component {
127129
render() {
128130
const Door = () => (
129131
<h1>I am a door!</h1>
@@ -264,14 +266,22 @@ export const TernaryComponent = () => {
264266
};
265267
`;
266268

267-
const ifBlock = /* tsx */ `
268-
export const IfBlockComponent = () => {
269-
const active = useIsActive();
270-
if (active) {
271-
return <ActiveComponent />;
272-
}
273-
return <div />;
274-
};
269+
const exportedError = /* tsx */ `
270+
const NotExported = () => <Foo />
271+
export const Exported = () => <Foo />
272+
export default DefaultExported = () => <Foo />
273+
const ExportedAtEnd = () => <Foo />
274+
const RenamedExport = () => <Foo />
275+
export { ExportedAtEnd, RenamedExport as ThisIsRenamed }
276+
`;
277+
278+
const exported = /* tsx */ `
279+
const NotExported = () => <Foo />
280+
export const Exported = () => <Foo data-component="Exported" />
281+
export default DefaultExported = () => <Foo data-component="DefaultExported" />
282+
const ExportedAtEnd = () => <Foo data-component="ExportedAtEnd" />
283+
const RenamedExport = () => <Foo data-component="RenamedExport" />
284+
export { ExportedAtEnd, RenamedExport as ThisIsRenamed }
275285
`;
276286

277287
const tests = {
@@ -344,7 +354,7 @@ const tests = {
344354
code: ternary,
345355
},
346356
{
347-
code: ifBlock,
357+
code: exported,
348358
},
349359
],
350360
invalid: [
@@ -427,6 +437,16 @@ const tests = {
427437
'InternalLink is missing the data-component attribute for the top-level element.',
428438
],
429439
},
440+
{
441+
code: exportedError,
442+
output: exported,
443+
errors: [
444+
'Exported is missing the data-component attribute for the top-level element.',
445+
'DefaultExported is missing the data-component attribute for the top-level element.',
446+
'ExportedAtEnd is missing the data-component attribute for the top-level element.',
447+
'RenamedExport is missing the data-component attribute for the top-level element.',
448+
],
449+
},
430450
],
431451
},
432452
},

0 commit comments

Comments
 (0)