Skip to content

Add fix for prefer-stateless-function #1229

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

Closed
wants to merge 8 commits into from
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
* [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md): Prevent definitions of unused prop types
* [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md): Prevent usage of `setState` in `componentWillUpdate`
* [react/prefer-es6-class](docs/rules/prefer-es6-class.md): Enforce ES5 or ES6 class for React Components
* [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless React Components to be written as a pure function
* [react/prefer-stateless-function](docs/rules/prefer-stateless-function.md): Enforce stateless React Components to be written as a pure function (fixable)
* [react/prop-types](docs/rules/prop-types.md): Prevent missing props validation in a React component definition
* [react/react-in-jsx-scope](docs/rules/react-in-jsx-scope.md): Prevent missing `React` when using JSX
* [react/require-default-props](docs/rules/require-default-props.md): Enforce a defaultProps definition for every prop that is not a required prop
Expand Down
128 changes: 128 additions & 0 deletions lib/rules/prefer-stateless-function/fixer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use strict';

const detectIndent = require('detect-indent');
const statefulComponentHandler = require('./statefulComponentHandler');

/**
* Gets indentation style of the file
* @param sourceCode
* @returns {string} one indentation example
*/
function getFileIndentation(sourceCode) {
return detectIndent(sourceCode.getText()).indent || ' ';
}

/**
* Removes every usage of this in properties
* Example: this.props -> props
* @param str
* @returns {string}
*/
function removeThisFromPropsUsages(str) {
const thisRegex = /this\.props/g;

return str.replace(thisRegex, 'props');
}

function ruleFixer(sourceCode, componentNode, utils) {
/**
* Returns string which is indented one level down
* @param str
* @returns {string}
*/
function indentOneLevelDown(str) {
const indentation = getFileIndentation(sourceCode);

return str
.split('\n')
.map((line) => line.replace(indentation, ''))
.join('\n');
}

/**
* Return how deep whole block is indented
* @param body {string} code block body
* @returns {string} base indentation
*/
function getBlockBaseIndentation(body) {
const lines = body.split('\n');
const lastLine = lines[lines.length - 1];
const matchIndentation = /^(\s*)[^\s]/g;

return matchIndentation.exec(lastLine)[1];
}

/**
* Returns correctly indented static props of the component
* @param staticProps {Array<string>}
* @param transformedBody {string}
* @returns {string}
*/
function getStaticPropsText(staticProps, transformedBody) {
const indentation = getBlockBaseIndentation(transformedBody);

return staticProps
.map((props) => indentation + indentOneLevelDown(props))
.join('\n');
}

/**
* Returns prepared body of the render function
* @param body {string} body
* @returns {string}
*/
function transformBody(body) {
if (!body) {
return '{}';
}

return removeThisFromPropsUsages(indentOneLevelDown(body));
}

/**
* Returns render of the stateless function
* @param name {string} name of the component
* @param transformedBody {string} body of the component with a curly braces
* @returns {string} stateless component body
*/
function getComponentText(name, transformedBody) {
return `function ${name}(props) ${transformedBody}`;
}

/**
* Returns concatenated values of the parts of component
* @param options {object}
* @param options.name {string} component name
* @param options.body {string} render body with curly braces
* @param options.staticProps {string[]} array of static properties attached to components
* @returns {string}
*/
function getComponent(options) {
const transformedBody = transformBody(options.body);
const componentText = getComponentText(options.name, transformedBody);
const staticProps = getStaticPropsText(options.staticProps, transformedBody);

if (staticProps) {
return `${componentText}\n${staticProps}`;
}

return componentText;
}

return function (fixer) {
if (utils.isES5Component(componentNode)) {
return;
}

var componentDetails = statefulComponentHandler(sourceCode, componentNode);

if (!componentDetails.name) {
return;
}

// eslint-disable-next-line
return fixer.replaceText(componentNode, getComponent(componentDetails));
};
}

module.exports = ruleFixer;
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/
'use strict';

var has = require('has');
var Components = require('../util/Components');
var versionUtil = require('../util/version');
const has = require('has');
const Components = require('../../util/Components');
const versionUtil = require('../../util/version');
const fixer = require('./fixer');

// ------------------------------------------------------------------------------
// Rule Definition
Expand All @@ -21,6 +22,7 @@ module.exports = {
category: 'Stylistic Issues',
recommended: false
},
fixable: 'code',
schema: [{
type: 'object',
properties: {
Expand Down Expand Up @@ -221,8 +223,14 @@ module.exports = {
property.kind === 'constructor' &&
isRedundantSuperCall(property.value.body.body, property.value.params)
;
var isStatic = property.static;
var isRender = name === 'render';
return !isDisplayName && !isPropTypes && !contextTypes && !isUselessConstructor && !isRender;
return !isDisplayName
&& !isPropTypes
&& !contextTypes
&& !isUselessConstructor
&& !isRender
&& !isStatic;
});
}

Expand Down Expand Up @@ -406,7 +414,8 @@ module.exports = {
}
context.report({
node: list[component].node,
message: 'Component should be written as a pure function'
message: 'Component should be written as a pure function',
fix: fixer(sourceCode, list[component].node, utils)
});
}
}
Expand Down
67 changes: 67 additions & 0 deletions lib/rules/prefer-stateless-function/statefulComponentHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict';

module.exports = function (sourceCode, componentNode) {
/**
* Returns name of the stateful component
*/
function getComponentName() {
return (componentNode.id && componentNode.id.name) || '';
}

/**
* Returns node of render definition
* @returns {*}
*/
function getRenderNode() {
return componentNode.body.body
.find(function (member) {
return member.type === 'MethodDefinition' && member.key.name === 'render';
});
}

/**
* Returns every static property defined in the component
* @returns {Array<string>} array of the properties
*/
function getStaticProps() {
function getProperties() {
return componentNode.body.body
.filter(function (property) {
return property.type === 'ClassProperty';
});
}

function getPropertyCode(property) {
return sourceCode.getText(property);
}

return getProperties()
.map(function (property) {
var staticKeywordRegex = /static /g;
var componentName = getComponentName();

return getPropertyCode(property)
.replace(staticKeywordRegex, `${componentName}.`);
});
}

/**
* Return body of the render function with curly braces
* @returns {undefined|string}
*/
function getRenderBody() {
var renderNode = getRenderNode();

if (!renderNode) {
return '';
}

return sourceCode.getText(renderNode.value.body);
}

return {
name: getComponentName(),
body: getRenderBody(),
staticProps: getStaticProps()
};
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"homepage": "https://github.com/yannickcr/eslint-plugin-react",
"bugs": "https://github.com/yannickcr/eslint-plugin-react/issues",
"dependencies": {
"detect-indent": "^5.0.0",
"doctrine": "^2.0.0",
"has": "^1.0.1",
"jsx-ast-utils": "^1.3.4"
Expand Down
Loading