Skip to content

Commit

Permalink
v2.0.0 RC1 (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
tatethurston authored Apr 29, 2022
1 parent 7ac1f3d commit 38e5acb
Show file tree
Hide file tree
Showing 9 changed files with 1,176 additions and 1,392 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
cache: "yarn"
registry-url: "https://registry.npmjs.org"
- run: yarn
- run: yarn build
- run: npm publish
- run: yarn package:build
- run: cd dist && npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
dist
coverage
.prettierignore
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## v2.0.0

Support for `createClass` has been dropped. Usage of `createClass` will no longer be detected.

Now errors on any JSX usage within a `class`. Previously the below was not caught:

```jsx
import Document from "next/document";

class MyDocument extends Document {
render() {
<>...</>;
}
}
```

## v1.0.0

No API changes. This library will now follow [semantic versioning](https://docs.npmjs.com/about-semantic-versioning).
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# eslint-plugin-react-prefer-function-component

<blockquote>ESLint lint rule to enforce function components in React</blockquote>
<blockquote>An [ESLint](https://github.com/eslint/eslint) plugin that prevents the use of React class components.</blockquote>

<br />

Expand All @@ -25,7 +25,7 @@

## What is this? 🧐

An [ESLint](https://github.com/eslint/eslint) plugin that prevents the use of React class components.
An [ESLint](https://github.com/eslint/eslint) plugin that prevents the use of React class components. While this plugin specifically calls out React, it will work with Preact, Inferno, or other JSX libraries.

## Motivation

Expand All @@ -48,11 +48,11 @@ This option is configurable.

> What about [eslint-plugin-react/prefer-stateless-function](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prefer-stateless-function.md)?
`eslint-plugin-react/prefer-stateless-function` allows class components that implement any class methods or properties. This rule is stricter and prevents the use of _any_ class components. See this [Stack Overflow question](https://stackoverflow.com/questions/63333796/how-to-use-react-with-function-component-and-hooks-only) for more context.
`eslint-plugin-react/prefer-stateless-function` allows class components that implement any methods or properties other than `render`. This rule is stricter and prevents the use of _any_ class components. This [open issue](https://github.com/jsx-eslint/eslint-plugin-react/issues/2860) explains the limitations of `prefer-stateless-function` and the motivations for this plugin.

> Why didn't you contribute this rule to [https://github.com/yannickcr/eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react)?
I'm discussing this in an [open issue](https://github.com/yannickcr/eslint-plugin-react/issues/2860#issuecomment-819784530) and [pull request](https://github.com/yannickcr/eslint-plugin-react/pull/3040) on `eslint-plugin-react`. At this time, the maintainer is unconvinced that function component enforcement should be a lint rule.
I'm discussing this in an [open issue](https://github.com/yannickcr/eslint-plugin-react/issues/2860#issuecomment-819784530) and [pull request](https://github.com/yannickcr/eslint-plugin-react/pull/3040) on `eslint-plugin-react`. At this time, the maintainer of `eslint-plugin-react` is unconvinced that function component enforcement should be a lint rule. If you would like to see this rule added to `eslint-plugin-react`, please join the discussion on the issue or pull request.

## Installation & Usage 📦

Expand Down
50 changes: 20 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"name": "eslint-plugin-react-prefer-function-component",
"version": "1.0.0",
"description": "ESLint lint rule to enforce function components in React",
"name": "eslint-plugin-react-prefer-function-component-development",
"description": "ESLint plugin that prevents the use of JSX class components",
"license": "MIT",
"author": "Tate <[email protected]>",
"homepage": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component#readme",
Expand All @@ -12,11 +11,6 @@
"bugs": {
"url": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component/issues"
},
"main": "dist/index.js",
"files": [
"dist/index.d.ts",
"dist/prefer-function-component/index.js"
],
"scripts": {
"build": "yarn clean && yarn tsc",
"build:watch": "yarn build --watch",
Expand All @@ -26,39 +20,35 @@
"lint:fix:md": "prettier --write '*.md'",
"lint:fix:package": "prettier-package-json --write package.json",
"lint:fix:ts": "eslint --fix './src/**/*.ts{,x}'",
"package:build": "yarn build && yarn package:prune && yarn package:copy:files",
"package:copy:files": "cp ./LICENSE ./README.md dist/ && cp ./public.package.json dist/package.json",
"package:prune": "find dist -name test.* -delete",
"test": "yarn jest src/*",
"test:ci": "yarn test --coverage",
"typecheck": "yarn tsc --noEmit",
"typecheck:watch": "yarn typecheck --watch"
},
"types": "dist/index.d.ts",
"devDependencies": {
"@babel/preset-env": "^7.14.1",
"@babel/preset-env": "^7.17.10",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@types/eslint": "^8.4.0",
"@types/estree": "^0.0.50",
"@types/jest": "^27.4.0",
"@types/node": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"@types/eslint": "^8.4.1",
"@types/estree": "^0.0.51",
"@types/jest": "^27.4.1",
"@types/node": "^17.0.30",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"codecov": "^3.8.3",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.5.0",
"husky": "^4.3.0",
"jest": "^27.4.7",
"prettier": "^2.5.1",
"prettier-package-json": "^2.6.0",
"typescript": "^4.5.5"
"jest": "^28.0.3",
"prettier": "^2.6.2",
"prettier-package-json": "^2.6.3",
"typescript": "^4.6.4"
},
"keywords": [
"eslint react no class",
"react function component",
"react functional component",
"react no class"
],
"husky": {
"hooks": {
"pre-commit": "yarn lint"
Expand Down
25 changes: 25 additions & 0 deletions public.package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "eslint-plugin-react-prefer-function-component",
"version": "2.0.0-rc1",
"description": "ESLint plugin that prevents the use of JSX class components",
"license": "MIT",
"author": "Tate <[email protected]>",
"homepage": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/tatethurston/eslint-plugin-react-prefer-function-component.git"
},
"bugs": {
"url": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component/issues"
},
"main": "index.js",
"types": "index.d.ts",
"keywords": [
"eslint react no class",
"eslint react class",
"lint react no class",
"lint react class",
"lint jsx no class",
"lint jsx class"
]
}
84 changes: 17 additions & 67 deletions src/prefer-function-component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@

import type { Rule } from "eslint";

// TODO:
// .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings)
// https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/util/pragma.js
const pragma = "React";
const createClass = "createReactClass";
export const COMPONENT_SHOULD_BE_FUNCTION = "componentShouldBeFunction";
export const ALLOW_COMPONENT_DID_CATCH = "allowComponentDidCatch";
const COMPONENT_DID_CATCH = "componentDidCatch";
Expand All @@ -21,34 +16,6 @@ type Node = any;

/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */

function getComponentProperties(node: Node): Node[] {
switch (node.type) {
case "ClassDeclaration":
case "ClassExpression":
return node.body.body;
case "ObjectExpression":
return node.properties;
default:
return [];
}
}

function getPropertyNameNode(node: Node): Node | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
if (node.key || ["MethodDefinition", "Property"].indexOf(node.type) !== -1) {
return node.key;
}
if (node.type === "MemberExpression") {
return node.property;
}
return undefined;
}

function getPropertyName(node: Node): string {
const nameNode = getPropertyNameNode(node);
return nameNode ? nameNode.name : "";
}

// https://eslint.org/docs/developer-guide/working-with-rules
const rule: Rule.RuleModule = {
meta: {
Expand Down Expand Up @@ -81,51 +48,34 @@ const rule: Rule.RuleModule = {
create(context: Rule.RuleContext) {
const allowComponentDidCatch =
context.options[0]?.allowComponentDidCatch ?? true;
const sourceCode = context.getSourceCode();

function isES5Component(node: Node): boolean {
if (!node.parent) {
return false;
}

return new RegExp(`^(${pragma}\\.)?${createClass}$`).test(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
sourceCode.getText(node.parent.callee)
);
}

function isES6Component(node: Node): boolean {
if (!node.superClass) {
return false;
}

return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
sourceCode.getText(node.superClass)
);
}

function shouldPreferFunction(node: Node): boolean {
if (!allowComponentDidCatch) {
return true;
const properties = node.body.body;
const hasComponentDidCatch =
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
properties.find(
(property: Node) => property.key?.name === COMPONENT_DID_CATCH
) !== undefined;

if (hasComponentDidCatch && allowComponentDidCatch) {
return false;
}

const properties = getComponentProperties(node).map(getPropertyName);
return !properties.includes(COMPONENT_DID_CATCH);
return true;
}

const components = new Set<Node>();

const detect = (guard: (node: Node) => boolean) => (node: Node) => {
if (guard(node) && shouldPreferFunction(node)) {
function detect(node: Node): void {
if (shouldPreferFunction(node)) {
components.add(node);
}
};
}

return {
ObjectExpression: detect(isES5Component),
ClassDeclaration: detect(isES6Component),
ClassExpression: detect(isES6Component),
"ClassDeclaration:has(JSXElement)": detect,
"ClassDeclaration:has(JSXFragment)": detect,
"ClassExpression:has(JSXElement)": detect,
"ClassExpression:has(JSXFragment)": detect,

[PROGRAM_EXIT]() {
components.forEach((node) => {
Expand Down
70 changes: 70 additions & 0 deletions src/prefer-function-component/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,28 @@ ruleTester.run("prefer-function-component", rule, {
};
`,
},
{
// class without JSX
code: `
class Foo {
render() {
return 'hello'
}
};
`,
},
{
// object with JSX
code: `
const foo = {
foo: <h>hello</h>
};
`,
},
],

invalid: [
// Extending from react
{
code: `
import { Component } from 'react';
Expand All @@ -86,6 +105,57 @@ ruleTester.run("prefer-function-component", rule, {
},
],
},
// Extending from preact
{
code: `
import { Component } from 'preact';
class Foo extends Component {
render() {
return <div>{this.props.foo}</div>;
}
}
`,
errors: [
{
messageId: COMPONENT_SHOULD_BE_FUNCTION,
},
],
},
// Extending from inferno
{
code: `
import { Component } from 'inferno';
class Foo extends Component {
render() {
return <div>{this.props.foo}</div>;
}
}
`,
errors: [
{
messageId: COMPONENT_SHOULD_BE_FUNCTION,
},
],
},
// Extending from another class (not Component)
{
code: `
import Document from 'next/document';
class Foo extends Document {
render() {
return <div>{this.props.foo}</div>;
}
}
`,
errors: [
{
messageId: COMPONENT_SHOULD_BE_FUNCTION,
},
],
},
{
code: `
class Foo extends React.Component {
Expand Down
Loading

0 comments on commit 38e5acb

Please sign in to comment.