Skip to content

Commit

Permalink
Tate/v1.0.0 (#3)
Browse files Browse the repository at this point in the history
* Revert "migrate to eslint-plugin-react internals (#1)"

This reverts commit 2f48be4.

* v1.0.0

Co-authored-by: Tate <tate@transcriptic.com>
  • Loading branch information
tatethurston and Tate authored Jan 20, 2022
1 parent b64ffcb commit e4ae67f
Show file tree
Hide file tree
Showing 12 changed files with 2,438 additions and 2,566 deletions.
44 changes: 11 additions & 33 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,18 @@ on:
branches:
- main
pull_request:

jobs:
lint:
ci:
name: "Lint and Test"
runs-on: ubuntu-latest
name: Lint
steps:
- name: Check out code
uses: actions/checkout@v2

- name: Setup NodeJS
uses: actions/setup-node@v2
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14

- name: Install Node Modules
run: yarn install

- name: Lint
run: yarn run lint

test:
runs-on: ubuntu-latest
name: Test
steps:
- name: Check out code
uses: actions/checkout@v2

- name: Setup NodeJS
uses: actions/setup-node@v2
with:
node-version: 14

- name: Install Node Modules
run: yarn install

- name: Test
run: yarn run test
node-version: "16.x"
cache: "yarn"
- run: yarn
- run: yarn lint
- run: yarn test:ci && yarn codecov --token=$CODECOV_TOKEN
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
19 changes: 19 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Publish NPM Package
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "16.x"
cache: "yarn"
registry-url: "https://registry.npmjs.org"
- run: yarn
- run: yarn build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## v1.0.0

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

<blockquote>ESLint lint rule to enforce function components in React</blockquote>

Expand All @@ -19,6 +19,9 @@
<a href="https://github.com/tatethurston/eslint-plugin-react-prefer-function-component/actions/workflows/ci.yml">
<img src="https://github.com/tatethurston/eslint-plugin-react-prefer-function-component/actions/workflows/ci.yml/badge.svg">
</a>
<a href="https://codecov.io/gh/tatethurston/eslint-plugin-react-prefer-function-component">
<img src="https://img.shields.io/codecov/c/github/tatethurston/eslint-plugin-react-prefer-function-component/main.svg?style=flat-square">
</a>

## What is this? 🧐

Expand Down
8 changes: 8 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// eslint-disable-next-line no-undef
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
"@babel/preset-react",
],
};
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// eslint-disable-next-line no-undef
module.exports = {
clearMocks: true,
coverageDirectory: "coverage",
testEnvironment: "node",
};
55 changes: 26 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "eslint-plugin-react-prefer-function-component",
"version": "1.0.0",
"description": "ESLint lint rule to enforce function components in React",
"license": "MIT",
"author": "Tate <tatethurston@gmail.com>",
Expand All @@ -11,9 +12,9 @@
"bugs": {
"url": "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component/issues"
},
"version": "0.0.7",
"main": "dist/index.js",
"files": [
"dist/index.d.ts",
"dist/prefer-function-component/index.js"
],
"scripts": {
Expand All @@ -25,42 +26,38 @@
"lint:fix:md": "prettier --write '*.md'",
"lint:fix:package": "prettier-package-json --write package.json",
"lint:fix:ts": "eslint --fix './src/**/*.ts{,x}'",
"test": "yarn build && jest --testTimeout 5000 --rootDir dist",
"test": "yarn jest src/*",
"test:ci": "yarn test --coverage",
"typecheck": "yarn tsc --noEmit",
"typecheck:watch": "yarn typecheck --watch",
"version": "yarn run build && git add -A package.json",
"postversion": "git push && git push --tags"
"typecheck:watch": "yarn typecheck --watch"
},
"types": "dist/index.d.ts",
"peerDependencies": {
"eslint-plugin-react": "^7.23.2"
},
"devDependencies": {
"@types/eslint": "^7.2.9",
"@types/estree": "^0.0.47",
"@types/jest": "^26.0.15",
"@types/node": "^13.13.5",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"eslint": "^7.8.1",
"@babel/preset-env": "^7.14.1",
"@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",
"codecov": "^3.8.3",
"eslint": "^8.7.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^4.3.0",
"jest": "^26.6.3",
"prettier": "^2.1.1",
"prettier-package-json": "^2.1.3",
"typescript": "^4.1.3"
"jest": "^27.4.7",
"prettier": "^2.5.1",
"prettier-package-json": "^2.6.0",
"typescript": "^4.5.5"
},
"keywords": [
"class component",
"eslint",
"eslint-plugin",
"eslintplugin",
"function component",
"functional component",
"no class",
"react"
"eslint react no class",
"react function component",
"react functional component",
"react no class"
],
"husky": {
"hooks": {
Expand Down
3 changes: 0 additions & 3 deletions src/eslint-plugin-react.d.ts

This file was deleted.

13 changes: 0 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,6 @@ module.exports = {
"error",
},
},
settings: {
react: {
// Regex for Component Factory to use (defaults to "createReactClass")
createClass: "createReactClass",
// Pragma to use (defaults to "React")
pragma: "React",
// Fragment to use (may be a property of <pragma>), default to "Fragment"
fragment: "Fragment",
// React version. "detect" automatically picks the version you have installed.
// You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
version: "detect",
},
},
},
rules: {
"react-prefer-function-component": PreferFunctionComponent,
Expand Down
108 changes: 78 additions & 30 deletions src/prefer-function-component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,53 @@
* @fileoverview Enforce components are written as function components
*/

// TODO: improve typing
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */

import type { Rule } from "eslint";
// Using eslint-plugin-react internals for upstream consideration
// https://github.com/yannickcr/eslint-plugin-react/issues/2860#issuecomment-819784530
import Components from "eslint-plugin-react/lib/util/Components";
import {
getComponentProperties,
getPropertyName,
} from "eslint-plugin-react/lib/util/ast";

// 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";
// https://eslint.org/docs/developer-guide/working-with-rules
const PROGRAM_EXIT = "Program:exit";

// TODO: Type definitions
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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 All @@ -28,8 +57,7 @@ const rule: Rule.RuleModule = {
category: "Stylistic Issues",
recommended: false,
suggestion: false,
url:
"https://github.com/tatethurston/eslint-plugin-react-prefer-function-component#rule-details",
url: "https://github.com/tatethurston/eslint-plugin-react-prefer-function-component#rule-details",
},
type: "problem",
messages: {
Expand All @@ -50,11 +78,34 @@ const rule: Rule.RuleModule = {
],
},

create: Components.detect((context: any, components: any, utils: any) => {
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;
}

function shouldPreferFunction(node: any): boolean {
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;
}
Expand All @@ -63,32 +114,29 @@ const rule: Rule.RuleModule = {
return !properties.includes(COMPONENT_DID_CATCH);
}

const detect = (guard: (node: any) => boolean) => (node: any) => {
const components = new Set<Node>();

const detect = (guard: (node: Node) => boolean) => (node: Node) => {
if (guard(node) && shouldPreferFunction(node)) {
components.set(node, {
[COMPONENT_SHOULD_BE_FUNCTION]: true,
});
components.add(node);
}
};

return {
ObjectExpression: detect(utils.isES5Component),
ClassDeclaration: detect(utils.isES6Component),
ClassExpression: detect(utils.isES6Component),
ObjectExpression: detect(isES5Component),
ClassDeclaration: detect(isES6Component),
ClassExpression: detect(isES6Component),

[PROGRAM_EXIT]() {
const list = components.list();
Object.values(list).forEach((component: any) => {
if (component[COMPONENT_SHOULD_BE_FUNCTION]) {
context.report({
node: component.node,
messageId: COMPONENT_SHOULD_BE_FUNCTION,
});
}
components.forEach((node) => {
context.report({
node,
messageId: COMPONENT_SHOULD_BE_FUNCTION,
});
});
},
};
}),
},
};

export default rule;
5 changes: 0 additions & 5 deletions src/prefer-function-component/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ const ruleTester = new RuleTester({
jsx: true,
},
},
settings: {
react: {
version: "latest",
},
},
});

ruleTester.run("prefer-function-component", rule, {
Expand Down
Loading

0 comments on commit e4ae67f

Please sign in to comment.