Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(enforce-ref-last-prop): add fixer #8671

Merged
merged 24 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
faa15d0
chore: add a fixer to custom ESLint rule to place ref last
Elijbet Jan 30, 2024
8ad0ddb
add disable line comment
Elijbet Jan 30, 2024
be407f6
Merge branch 'main' into elijbet/8080-place-ref-last-ESLint-fixer
Elijbet Jan 30, 2024
7425a3d
add good test case
Elijbet Jan 30, 2024
dd62176
remove any existing // eslint-disable-next-line ... above an existing…
Elijbet Feb 1, 2024
128fe41
Merge branch 'main' into elijbet/8080-place-ref-last-ESLint-fixer
Elijbet Feb 1, 2024
437bf32
update tests to cover the fixer, shorten the explainer comment
Elijbet Feb 1, 2024
c51f3c3
test eslint
Elijbet Feb 1, 2024
5f0b06a
eslint test
Elijbet Feb 2, 2024
db446de
fix formatting and undo the test
Elijbet Feb 2, 2024
4a5eda5
cleanup
Elijbet Feb 2, 2024
ef0d687
add output.tsx file
Elijbet Feb 2, 2024
8161d5f
Merge branch 'main' into elijbet/8080-place-ref-last-ESLint-fixer
Elijbet Feb 8, 2024
026be41
Merge branch 'main' into elijbet/8080-place-ref-last-ESLint-fixer
Elijbet Feb 26, 2024
8e3f724
dry up the code and match comment indentation with previous line
Elijbet Feb 26, 2024
4a88d30
cleanup and indentation workaround
Elijbet Apr 5, 2024
a9df312
fix alignment
jcfranco Apr 5, 2024
76c11ae
update tests
jcfranco Apr 5, 2024
11c041e
counter for the case when ref is the last and needs a disable rule line
Elijbet Apr 5, 2024
e65260b
update rule to check for ref prop to be last with accompanying disabl…
jcfranco Apr 6, 2024
0536456
fix boolean value to match its var name
jcfranco Apr 6, 2024
4c6a574
add one more test case
jcfranco Apr 6, 2024
a29fb3a
tidy up
jcfranco Apr 6, 2024
f40ee60
update doc
jcfranco Apr 6, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -648,8 +648,7 @@ describe("calcite-button", () => {
t9n("calcite-button");
});

describe('automatic tooltip', ()=>{

describe("automatic tooltip", () => {
it("shows tooltip for buttons with truncated long text", async () => {
const shortText = "Hi!";
const longText =
Expand Down Expand Up @@ -685,7 +684,6 @@ describe("calcite-button", () => {

expect(button).not.toHaveAttribute("title");
});

});

it("should set aria-expanded attribute on shadowDOM element when used as trigger", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ No config is needed
```json
{ "@esri/calcite-components/enforce-ref-last-prop": "error" }
```

> Fix included
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Rule } from "eslint";
import type { JSXAttribute, JSXSpreadAttribute, JSXOpeningElement } from "@babel/types";
import type { JSXAttribute, JSXOpeningElement, JSXSpreadAttribute } from "@babel/types";

const rule: Rule.RuleModule = {
meta: {
Expand All @@ -17,21 +17,46 @@ const rule: Rule.RuleModule = {
JSXIdentifier(node) {
const openingElement = node.parent as JSXOpeningElement;
if (openingElement.type === "JSXOpeningElement") {
const attributes: string[] = [];
const attributes = openingElement.attributes
.map((attr) => {
if (attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier") {
return attr;
}
})
.filter(Boolean);

openingElement.attributes.forEach((attr: JSXAttribute | JSXSpreadAttribute) => {
if (attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier") {
attributes.push(attr.name.name);
}
});
const refAttribute = attributes.find(
(attr: JSXAttribute | JSXSpreadAttribute) =>
attr.type === "JSXAttribute" && attr.name?.type === "JSXIdentifier" && attr.name.name === "ref",
);

const refAttribute = attributes.find((attr: string) => attr === "ref");
if (refAttribute) {
const { sourceCode } = context;
const refAttrText = sourceCode.getText(refAttribute as typeof node);
const otherAttrs = attributes.filter((attr) => attr !== refAttribute);
const indentation = new Array(refAttribute.loc.start.column).fill(" ").join("");
const tokenBeforeRefAttr = sourceCode.getTokenBefore(refAttribute as typeof node);
const eslintDisableComments = sourceCode
.getCommentsBefore(refAttribute as typeof node)
.filter((comment) => comment.value.includes("eslint-disable-next-line"));
const refIsLastWithSortDisablingComment =
attributes.indexOf(refAttribute) === attributes.length - 1 && eslintDisableComments.length !== 0;

if (refAttribute && attributes.indexOf(refAttribute) !== attributes.length - 1) {
context.report({
node,
message: `"ref" prop should be placed last in JSX to ensure the node attrs/props are in sync.`,
});
if (!refIsLastWithSortDisablingComment) {
context.report({
node,
message: `"ref" prop should be placed last in JSX to ensure the node attrs/props are in sync.`,
fix(fixer) {
return [
fixer.removeRange([tokenBeforeRefAttr.range[1], refAttribute.range[1]]),
fixer.insertTextAfterRange(
otherAttrs[otherAttrs.length - 1].range,
`\n${indentation}// eslint-disable-next-line react/jsx-sort-props -- auto-generated by @esri/calcite-components/enforce-ref-last-prop\n${indentation}${refAttrText}`,
),
];
},
});
}
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ export class SampleTag {
<Host>
<div
class="some-class"
id={`${guid}-element`}
id="element"
onClick={() => {
/* click! */
}}
tabIndex={0}
// eslint-disable-next-line enforce-ref-last-prop -- auto-generated by @esri/calcite-components/enforce-ref-last-prop
ref={(el: HTMLDivElement): void => {
/* refEl */
}}
>
test
test Note: we are intentionally specifying `enforce-ref-last-prop` in the disable line as
RuleTester can't be configured with multiple rules and this does not disable the custom
rule, please see the output file for the correctly generated disable line
</div>
</Host>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @ts-nocheck
@Component({ tag: "sample-tag" })
export class SampleTag {
render() {
return (
<Host>
<div
class="some-class"
id="use-case-1"
onClick={() => {
/* click! */
}}
tabIndex={0}
// eslint-disable-next-line react/jsx-sort-props -- auto-generated by @esri/calcite-components/enforce-ref-last-prop
ref={(el: HTMLDivElement): void => {
/* refEl */
}}
>
case where ref is not last prop
</div>
<div
class="some-class"
id="use-case-2"
onClick={() => {
/* click! */
}}
tabIndex={0}
// eslint-disable-next-line react/jsx-sort-props -- auto-generated by @esri/calcite-components/enforce-ref-last-prop
ref={(el: HTMLDivElement): void => {
/* refEl */
}}
>
case where ref last prop, but not commented
</div>
<div
class="some-class"
id="use-case-3"
onClick={() => {
/* click! */
}}
tabIndex={0}
// eslint-disable-next-line react/jsx-sort-props -- auto-generated by @esri/calcite-components/enforce-ref-last-prop
ref={(el: HTMLDivElement): void => {
/* refEl */
}}
>
case where ref last prop, and already commented Note: this is marked as wrong because
RuleTester can't configure multiple rules, so we ignore the ESLint error from not finding
the disabled rule
</div>
<div
class="some-class"
id="use-case-4"
onClick={() => {
/* click! */
}}
tabIndex={0}
// eslint-disable-next-line react/jsx-sort-props -- auto-generated by @esri/calcite-components/enforce-ref-last-prop
ref={(el: HTMLDivElement): void => {
/* refEl */
}}
>
case where ref is not last prop and already commented Note: this is marked as wrong
because RuleTester can't configure multiple rules, so we ignore the ESLint error from not
finding the disabled rule
</div>
</Host>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe("enforce-ref-last-prop rule", () => {
const files = {
good: path.resolve(__dirname, "enforce-ref-last-prop.good.tsx"),
wrong: path.resolve(__dirname, "enforce-ref-last-prop.wrong.tsx"),
output: path.resolve(__dirname, "enforce-ref-last-prop.output.tsx"),
};
ruleTester(projectPath).run("enforce-ref-last-prop", rule, {
valid: [
Expand All @@ -22,7 +23,25 @@ describe("enforce-ref-last-prop rule", () => {
{
code: fs.readFileSync(files.wrong, "utf8"),
filename: files.wrong,
errors: 1,
errors: [
// we include the disabled rule not found error because RuleTester doesn't support multiple rules
{
message: `"ref" prop should be placed last in JSX to ensure the node attrs/props are in sync.`,
},
{
message: `"ref" prop should be placed last in JSX to ensure the node attrs/props are in sync.`,
},
{
message: `Definition for rule 'react/jsx-sort-props' was not found.`,
},
{
message: `"ref" prop should be placed last in JSX to ensure the node attrs/props are in sync.`,
},
{
message: `Definition for rule 'react/jsx-sort-props' was not found.`,
},
],
output: fs.readFileSync(files.output, "utf8"),
},
],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,58 @@ export class SampleTag {
/* refEl */
}}
class="some-class"
id={`${guid}-element`}
id="use-case-1"
onClick={() => {
/* click! */
}}
tabIndex={0}
>
test
case where ref is not last prop
</div>
<div
class="some-class"
id="use-case-2"
onClick={() => {
/* click! */
}}
tabIndex={0}
ref={(el: HTMLDivElement): void => {
/* refEl */
}}
>
case where ref last prop, but not commented
jcfranco marked this conversation as resolved.
Show resolved Hide resolved
</div>
<div
class="some-class"
id="use-case-3"
onClick={() => {
/* click! */
}}
tabIndex={0}
// eslint-disable-next-line react/jsx-sort-props -- auto-generated by @esri/calcite-components/enforce-ref-last-prop
ref={(el: HTMLDivElement): void => {
/* refEl */
}}
>
case where ref last prop, and already commented Note: this is marked as wrong because
RuleTester can't configure multiple rules, so we ignore the ESLint error from not finding
the disabled rule
</div>
<div
// eslint-disable-next-line react/jsx-sort-props -- auto-generated by @esri/calcite-components/enforce-ref-last-prop
ref={(el: HTMLDivElement): void => {
/* refEl */
}}
class="some-class"
id="use-case-4"
onClick={() => {
/* click! */
}}
tabIndex={0}
>
case where ref is not last prop and already commented Note: this is marked as wrong
because RuleTester can't configure multiple rules, so we ignore the ESLint error from not
finding the disabled rule
</div>
</Host>
);
Expand Down
Loading