eslint-plugin-react
Advanced tools
Comparing version 7.29.4 to 7.30.0
@@ -41,2 +41,3 @@ 'use strict'; | ||
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'), | ||
'jsx-no-leaked-render': require('./lib/rules/jsx-no-leaked-render'), | ||
'jsx-no-literals': require('./lib/rules/jsx-no-literals'), | ||
@@ -43,0 +44,0 @@ 'jsx-no-script-url': require('./lib/rules/jsx-no-script-url'), |
@@ -129,3 +129,3 @@ /** | ||
if (typeProp.value.type === 'JSXExpressionContainer') { | ||
if (typeProp.value && typeProp.value.type === 'JSXExpressionContainer') { | ||
checkExpression(node, typeProp.value.expression); | ||
@@ -132,0 +132,0 @@ return; |
@@ -53,2 +53,3 @@ /** | ||
useDestructAssignment: 'Must use destructuring {{type}} assignment', | ||
destructureInSignature: 'Must destructure props in the function signature.', | ||
}; | ||
@@ -64,3 +65,3 @@ | ||
}, | ||
fixable: 'code', | ||
messages, | ||
@@ -80,2 +81,9 @@ | ||
}, | ||
destructureInSignature: { | ||
type: 'string', | ||
enum: [ | ||
'always', | ||
'ignore', | ||
], | ||
}, | ||
}, | ||
@@ -89,2 +97,3 @@ additionalProperties: false, | ||
const ignoreClassFields = (context.options[1] && (context.options[1].ignoreClassFields === true)) || false; | ||
const destructureInSignature = (context.options[1] && context.options[1].destructureInSignature) || 'ignore'; | ||
const sfcParams = createSFCParams(); | ||
@@ -237,2 +246,37 @@ | ||
} | ||
if ( | ||
SFCComponent | ||
&& destructuringSFC | ||
&& configuration === 'always' | ||
&& destructureInSignature === 'always' | ||
&& node.init.name === 'props' | ||
) { | ||
const scopeSetProps = context.getScope().set.get('props'); | ||
const propsRefs = scopeSetProps && scopeSetProps.references; | ||
if (!propsRefs) { | ||
return; | ||
} | ||
// Skip if props is used elsewhere | ||
if (propsRefs.length > 1) { | ||
return; | ||
} | ||
report(context, messages.destructureInSignature, 'destructureInSignature', { | ||
node, | ||
fix(fixer) { | ||
const param = SFCComponent.node.params[0]; | ||
if (!param) { | ||
return; | ||
} | ||
const replaceRange = [ | ||
param.range[0], | ||
param.typeAnnotation ? param.typeAnnotation.range[0] : param.range[1], | ||
]; | ||
return [ | ||
fixer.replaceTextRange(replaceRange, context.getSourceCode().getText(node.id)), | ||
fixer.remove(node.parent), | ||
]; | ||
}, | ||
}); | ||
} | ||
}, | ||
@@ -239,0 +283,0 @@ }; |
@@ -12,2 +12,3 @@ /** | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -110,3 +111,3 @@ const propsUtil = require('../util/props'); | ||
&& (node.parent.type === 'VariableDeclarator' || node.parent.type === 'Property' || node.parent.method === true) | ||
&& (!node.parent.parent || !utils.isES5Component(node.parent.parent)) | ||
&& (!node.parent.parent || !componentUtil.isES5Component(node.parent.parent, context)) | ||
); | ||
@@ -197,3 +198,3 @@ | ||
ObjectExpression(node) { | ||
if (!utils.isES5Component(node)) { | ||
if (!componentUtil.isES5Component(node, context)) { | ||
return; | ||
@@ -200,0 +201,0 @@ } |
@@ -18,4 +18,6 @@ /** | ||
function buildFunction(template, parts) { | ||
return Object.keys(parts) | ||
.reduce((acc, key) => acc.replace(`{${key}}`, () => (parts[key] || '')), template); | ||
return Object.keys(parts).reduce( | ||
(acc, key) => acc.replace(`{${key}}`, () => parts[key] || ''), | ||
template | ||
); | ||
} | ||
@@ -25,4 +27,4 @@ | ||
'function-declaration': 'function {name}{typeParams}({params}){returnType} {body}', | ||
'arrow-function': 'var {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}', | ||
'function-expression': 'var {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}', | ||
'arrow-function': '{varType} {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}', | ||
'function-expression': '{varType} {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}', | ||
}; | ||
@@ -37,3 +39,6 @@ | ||
if (node.typeParameters) { | ||
return node.typeParameters.params.length === 1 && !node.typeParameters.params[0].constraint; | ||
return ( | ||
node.typeParameters.params.length === 1 | ||
&& !node.typeParameters.params[0].constraint | ||
); | ||
} | ||
@@ -45,3 +50,6 @@ | ||
function hasName(node) { | ||
return node.type === 'FunctionDeclaration' || node.parent.type === 'VariableDeclarator'; | ||
return ( | ||
node.type === 'FunctionDeclaration' | ||
|| node.parent.type === 'VariableDeclarator' | ||
); | ||
} | ||
@@ -59,3 +67,6 @@ | ||
if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') { | ||
if ( | ||
node.type === 'ArrowFunctionExpression' | ||
|| node.type === 'FunctionExpression' | ||
) { | ||
return hasName(node) && node.parent.id.name; | ||
@@ -67,3 +78,6 @@ } | ||
if (node.params.length === 0) return null; | ||
return source.slice(node.params[0].range[0], node.params[node.params.length - 1].range[1]); | ||
return source.slice( | ||
node.params[0].range[0], | ||
node.params[node.params.length - 1].range[1] | ||
); | ||
} | ||
@@ -75,7 +89,3 @@ | ||
if (node.body.type !== 'BlockStatement') { | ||
return [ | ||
'{', | ||
` return ${source.slice(range[0], range[1])}`, | ||
'}', | ||
].join('\n'); | ||
return ['{', ` return ${source.slice(range[0], range[1])}`, '}'].join('\n'); | ||
} | ||
@@ -89,3 +99,6 @@ | ||
if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') { | ||
if ( | ||
node.type === 'ArrowFunctionExpression' | ||
|| node.type === 'FunctionExpression' | ||
) { | ||
return getNodeText(node.parent.id.typeAnnotation, source); | ||
@@ -96,3 +109,7 @@ } | ||
function isUnfixableBecauseOfExport(node) { | ||
return node.type === 'FunctionDeclaration' && node.parent && node.parent.type === 'ExportDefaultDeclaration'; | ||
return ( | ||
node.type === 'FunctionDeclaration' | ||
&& node.parent | ||
&& node.parent.type === 'ExportDefaultDeclaration' | ||
); | ||
} | ||
@@ -128,8 +145,18 @@ | ||
oneOf: [ | ||
{ enum: ['function-declaration', 'arrow-function', 'function-expression'] }, | ||
{ | ||
enum: [ | ||
'function-declaration', | ||
'arrow-function', | ||
'function-expression', | ||
], | ||
}, | ||
{ | ||
type: 'array', | ||
items: { | ||
type: 'string', | ||
enum: ['function-declaration', 'arrow-function', 'function-expression'], | ||
enum: [ | ||
'function-declaration', | ||
'arrow-function', | ||
'function-expression', | ||
], | ||
}, | ||
@@ -158,5 +185,10 @@ }, | ||
const configuration = context.options[0] || {}; | ||
let fileVarType = 'var'; | ||
const namedConfig = [].concat(configuration.namedComponents || 'function-declaration'); | ||
const unnamedConfig = [].concat(configuration.unnamedComponents || 'function-expression'); | ||
const namedConfig = [].concat( | ||
configuration.namedComponents || 'function-declaration' | ||
); | ||
const unnamedConfig = [].concat( | ||
configuration.unnamedComponents || 'function-expression' | ||
); | ||
@@ -169,15 +201,30 @@ function getFixer(node, options) { | ||
if (options.type === 'function-declaration' && typeAnnotation) return; | ||
if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) return; | ||
if (options.type === 'function-declaration' && typeAnnotation) { | ||
return; | ||
} | ||
if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) { | ||
return; | ||
} | ||
if (isUnfixableBecauseOfExport(node)) return; | ||
if (isFunctionExpressionWithName(node)) return; | ||
let varType = fileVarType; | ||
if ( | ||
(node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') | ||
&& node.parent.type === 'VariableDeclarator' | ||
) { | ||
varType = node.parent.parent.kind; | ||
} | ||
return (fixer) => fixer.replaceTextRange(options.range, buildFunction(options.template, { | ||
typeAnnotation, | ||
typeParams: getNodeText(node.typeParameters, source), | ||
params: getParams(node, source), | ||
returnType: getNodeText(node.returnType, source), | ||
body: getBody(node, source), | ||
name: getName(node), | ||
})); | ||
return (fixer) => fixer.replaceTextRange( | ||
options.range, | ||
buildFunction(options.template, { | ||
typeAnnotation, | ||
typeParams: getNodeText(node.typeParameters, source), | ||
params: getParams(node, source), | ||
returnType: getNodeText(node.returnType, source), | ||
body: getBody(node, source), | ||
name: getName(node), | ||
varType, | ||
}) | ||
); | ||
} | ||
@@ -203,5 +250,6 @@ | ||
template: NAMED_FUNCTION_TEMPLATES[namedConfig[0]], | ||
range: node.type === 'FunctionDeclaration' | ||
? node.range | ||
: node.parent.parent.range, | ||
range: | ||
node.type === 'FunctionDeclaration' | ||
? node.range | ||
: node.parent.parent.range, | ||
}, | ||
@@ -225,9 +273,26 @@ }); | ||
// -------------------------------------------------------------------------- | ||
const validatePairs = []; | ||
let hasES6OrJsx = false; | ||
return { | ||
FunctionDeclaration(node) { validate(node, 'function-declaration'); }, | ||
ArrowFunctionExpression(node) { validate(node, 'arrow-function'); }, | ||
FunctionExpression(node) { validate(node, 'function-expression'); }, | ||
FunctionDeclaration(node) { | ||
validatePairs.push([node, 'function-declaration']); | ||
}, | ||
ArrowFunctionExpression(node) { | ||
validatePairs.push([node, 'arrow-function']); | ||
}, | ||
FunctionExpression(node) { | ||
validatePairs.push([node, 'function-expression']); | ||
}, | ||
VariableDeclaration(node) { | ||
hasES6OrJsx = hasES6OrJsx || node.kind === 'const' || node.kind === 'let'; | ||
}, | ||
'Program:exit'() { | ||
if (hasES6OrJsx) fileVarType = 'const'; | ||
validatePairs.forEach((pair) => validate(pair[0], pair[1])); | ||
}, | ||
'ImportDeclaration, ExportNamedDeclaration, ExportDefaultDeclaration, ExportAllDeclaration, ExportSpecifier, ExportDefaultSpecifier, JSXElement, TSExportAssignment, TSImportEqualsDeclaration'() { | ||
hasES6OrJsx = true; | ||
}, | ||
}; | ||
}), | ||
}; |
@@ -69,9 +69,13 @@ /** | ||
const expectedSetterVariableName = valueVariableName ? ( | ||
`set${valueVariableName.charAt(0).toUpperCase()}${valueVariableName.slice(1)}` | ||
) : undefined; | ||
const caseCandidateMatch = valueVariableName ? valueVariableName.match(/(^[a-z]+)(.*)/) : undefined; | ||
const upperCaseCandidatePrefix = caseCandidateMatch ? caseCandidateMatch[1] : undefined; | ||
const caseCandidateSuffix = caseCandidateMatch ? caseCandidateMatch[2] : undefined; | ||
const expectedSetterVariableNames = upperCaseCandidatePrefix ? [ | ||
`set${upperCaseCandidatePrefix.charAt(0).toUpperCase()}${upperCaseCandidatePrefix.slice(1)}${caseCandidateSuffix}`, | ||
`set${upperCaseCandidatePrefix.toUpperCase()}${caseCandidateSuffix}`, | ||
] : []; | ||
const isSymmetricGetterSetterPair = valueVariable | ||
&& setterVariable | ||
&& setterVariableName === expectedSetterVariableName | ||
&& expectedSetterVariableNames.indexOf(setterVariableName) !== -1 | ||
&& variableNodes.length === 2; | ||
@@ -84,5 +88,9 @@ | ||
fix: (fixer) => { | ||
if (expectedSetterVariableNames.length === 0) { | ||
return; | ||
} | ||
const fix = fixer.replaceTextRange( | ||
node.parent.id.range, | ||
`[${valueVariableName}, ${expectedSetterVariableName}]` | ||
`[${valueVariableName}, ${expectedSetterVariableNames[0]}]` | ||
); | ||
@@ -89,0 +97,0 @@ |
@@ -39,3 +39,2 @@ /** | ||
const jsxUtil = require('../util/jsx'); | ||
const isCreateElement = require('../util/isCreateElement'); | ||
@@ -432,3 +431,3 @@ // ------------------------------------------------------------------------------ | ||
!fn | ||
|| !jsxUtil.isReturningJSX((n) => isCreateElement(n, context), node, context, true) | ||
|| !jsxUtil.isReturningJSX(node, context, true) | ||
) { | ||
@@ -435,0 +434,0 @@ return; |
@@ -73,3 +73,3 @@ /** | ||
if (jsxUtil.isJSX(node)) { | ||
count++; | ||
count += 1; | ||
} | ||
@@ -93,5 +93,3 @@ } | ||
function find(refs, prevRefs) { | ||
let i = refs.length; | ||
while (--i >= 0) { | ||
for (let i = refs.length - 1; i >= 0; i--) { | ||
if (has(refs[i], 'writeExpr')) { | ||
@@ -126,3 +124,3 @@ const writeExpr = refs[i].writeExpr; | ||
function checkDescendant(baseDepth, children) { | ||
baseDepth++; | ||
baseDepth += 1; | ||
(children || []).forEach((node) => { | ||
@@ -129,0 +127,0 @@ if (!hasJSX(node)) { |
@@ -11,3 +11,2 @@ /** | ||
const propName = require('jsx-ast-utils/propName'); | ||
const Components = require('../util/Components'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -67,3 +66,3 @@ const jsxUtil = require('../util/jsx'); | ||
create: Components.detect((context) => { | ||
create(context) { | ||
const configuration = context.options[0] || {}; | ||
@@ -203,3 +202,3 @@ | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -82,3 +82,3 @@ /** | ||
const messages = { | ||
NeedsMoreChildren: 'Fragments should contain more than one child - otherwise, there‘s no need for a Fragment at all.', | ||
NeedsMoreChildren: 'Fragments should contain more than one child - otherwise, there’s no need for a Fragment at all.', | ||
ChildOfHtmlElement: 'Passing a fragment to an HTML element is useless.', | ||
@@ -85,0 +85,0 @@ }; |
@@ -158,3 +158,3 @@ /** | ||
} | ||
index++; | ||
index += 1; | ||
} while (index < checkNames.length && !allowNamespace); | ||
@@ -161,0 +161,0 @@ }, |
@@ -136,3 +136,3 @@ /** | ||
) { | ||
groupCount++; | ||
groupCount += 1; | ||
sortableAttributeGroups[groupCount - 1] = []; | ||
@@ -139,0 +139,0 @@ } |
@@ -19,2 +19,3 @@ /** | ||
beforeSelfCloseNeedSpace: 'A space is required before closing bracket', | ||
beforeSelfCloseNeedNewline: 'A newline is required before closing bracket', | ||
afterOpenNoSpace: 'A space is forbidden after opening bracket', | ||
@@ -24,2 +25,3 @@ afterOpenNeedSpace: 'A space is required after opening bracket', | ||
beforeCloseNeedSpace: 'Whitespace is required before closing bracket', | ||
beforeCloseNeedNewline: 'A newline is required before closing bracket', | ||
}; | ||
@@ -104,2 +106,15 @@ | ||
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') { | ||
if (leftToken.loc.end.line === closingSlash.loc.start.line) { | ||
report(context, messages.beforeSelfCloseNeedNewline, 'beforeSelfCloseNeedNewline', { | ||
node, | ||
loc: leftToken.loc.end, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(closingSlash, '\n'); | ||
}, | ||
}); | ||
return; | ||
} | ||
} | ||
if (leftToken.loc.end.line !== closingSlash.loc.start.line) { | ||
@@ -109,3 +124,5 @@ return; | ||
if (option === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { | ||
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash); | ||
if ((option === 'always' || option === 'proportional-always') && adjacent) { | ||
report(context, messages.beforeSelfCloseNeedSpace, 'beforeSelfCloseNeedSpace', { | ||
@@ -118,3 +135,3 @@ node, | ||
}); | ||
} else if (option === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { | ||
} else if (option === 'never' && !adjacent) { | ||
report(context, messages.beforeSelfCloseNoSpace, 'beforeSelfCloseNoSpace', { | ||
@@ -174,6 +191,20 @@ node, | ||
const sourceCode = context.getSourceCode(); | ||
const lastTokens = sourceCode.getLastTokens(node, 2); | ||
const closingToken = lastTokens[1]; | ||
const leftToken = lastTokens[0]; | ||
const leftToken = option === 'proportional-always' | ||
? getTokenBeforeClosingBracket(node) | ||
: sourceCode.getLastTokens(node, 2)[0]; | ||
const closingToken = sourceCode.getTokenAfter(leftToken); | ||
if (node.loc.start.line !== node.loc.end.line && option === 'proportional-always') { | ||
if (leftToken.loc.end.line === closingToken.loc.start.line) { | ||
report(context, messages.beforeCloseNeedNewline, 'beforeCloseNeedNewline', { | ||
node, | ||
loc: leftToken.loc.end, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(closingToken, '\n'); | ||
}, | ||
}); | ||
return; | ||
} | ||
} | ||
if (leftToken.loc.start.line !== closingToken.loc.start.line) { | ||
@@ -207,2 +238,13 @@ return; | ||
}); | ||
} else if (option === 'proportional-always' && node.type === 'JSXOpeningElement' && adjacent !== (node.loc.start.line === node.loc.end.line)) { | ||
report(context, messages.beforeCloseNeedSpace, 'beforeCloseNeedSpace', { | ||
node, | ||
loc: { | ||
start: leftToken.loc.end, | ||
end: closingToken.loc.start, | ||
}, | ||
fix(fixer) { | ||
return fixer.insertTextBefore(closingToken, ' '); | ||
}, | ||
}); | ||
} | ||
@@ -243,3 +285,3 @@ } | ||
beforeSelfClosing: { | ||
enum: ['always', 'never', 'allow'], | ||
enum: ['always', 'proportional-always', 'never', 'allow'], | ||
}, | ||
@@ -250,3 +292,3 @@ afterOpening: { | ||
beforeClosing: { | ||
enum: ['always', 'never', 'allow'], | ||
enum: ['always', 'proportional-always', 'never', 'allow'], | ||
}, | ||
@@ -253,0 +295,0 @@ }, |
@@ -12,2 +12,3 @@ /** | ||
const reportC = require('../util/report'); | ||
const isParenthesized = require('../util/ast').isParenthesized; | ||
@@ -93,16 +94,6 @@ // ------------------------------------------------------------------------------ | ||
function isParenthesised(node) { | ||
const sourceCode = context.getSourceCode(); | ||
const previousToken = sourceCode.getTokenBefore(node); | ||
const nextToken = sourceCode.getTokenAfter(node); | ||
return previousToken && nextToken | ||
&& previousToken.value === '(' && previousToken.range[1] <= node.range[0] | ||
&& nextToken.value === ')' && nextToken.range[0] >= node.range[1]; | ||
} | ||
function needsOpeningNewLine(node) { | ||
const previousToken = context.getSourceCode().getTokenBefore(node); | ||
if (!isParenthesised(node)) { | ||
if (!isParenthesized(context, node)) { | ||
return false; | ||
@@ -121,3 +112,3 @@ } | ||
if (!isParenthesised(node)) { | ||
if (!isParenthesized(context, node)) { | ||
return false; | ||
@@ -159,3 +150,3 @@ } | ||
if ((option === true || option === 'parens') && !isParenthesised(node) && isMultilines(node)) { | ||
if ((option === true || option === 'parens') && !isParenthesized(context, node) && isMultilines(node)) { | ||
report(node, 'missingParens', (fixer) => fixer.replaceText(node, `(${sourceCode.getText(node)})`)); | ||
@@ -165,3 +156,3 @@ } | ||
if (option === 'parens-new-line' && isMultilines(node)) { | ||
if (!isParenthesised(node)) { | ||
if (!isParenthesized(context, node)) { | ||
const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true }); | ||
@@ -168,0 +159,0 @@ const tokenAfter = sourceCode.getTokenAfter(node, { includeComments: true }); |
@@ -9,3 +9,3 @@ /** | ||
const docsUrl = require('../util/docsUrl'); | ||
const Components = require('../util/Components'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const report = require('../util/report'); | ||
@@ -33,3 +33,3 @@ | ||
create: Components.detect((context, components, utils) => { | ||
create(context) { | ||
function isSetStateCall(node) { | ||
@@ -53,3 +53,3 @@ return node.type === 'CallExpression' | ||
function isClassComponent() { | ||
return !!(utils.getParentES6Component() || utils.getParentES5Component()); | ||
return !!(componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context)); | ||
} | ||
@@ -189,3 +189,3 @@ | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -12,2 +12,3 @@ /** | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -48,3 +49,3 @@ const lifecycleMethods = require('../util/lifecycleMethods'); | ||
create: Components.detect((context, components, utils) => { | ||
create: Components.detect((context, components) => { | ||
/** | ||
@@ -62,3 +63,3 @@ * @param {Array} properties list of component properties | ||
const isLifecycleMethod = ( | ||
node.static && !utils.isES5Component(node) | ||
node.static && !componentUtil.isES5Component(node, context) | ||
? lifecycleMethods.static | ||
@@ -65,0 +66,0 @@ : lifecycleMethods.instance |
@@ -11,5 +11,4 @@ /** | ||
const values = require('object.values'); | ||
const Components = require('../util/Components'); | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -33,2 +32,59 @@ const pragmaUtil = require('../util/pragma'); | ||
function getDeprecated(pragma) { | ||
const deprecated = {}; | ||
// 0.12.0 | ||
deprecated[`${pragma}.renderComponent`] = ['0.12.0', `${pragma}.render`]; | ||
deprecated[`${pragma}.renderComponentToString`] = ['0.12.0', `${pragma}.renderToString`]; | ||
deprecated[`${pragma}.renderComponentToStaticMarkup`] = ['0.12.0', `${pragma}.renderToStaticMarkup`]; | ||
deprecated[`${pragma}.isValidComponent`] = ['0.12.0', `${pragma}.isValidElement`]; | ||
deprecated[`${pragma}.PropTypes.component`] = ['0.12.0', `${pragma}.PropTypes.element`]; | ||
deprecated[`${pragma}.PropTypes.renderable`] = ['0.12.0', `${pragma}.PropTypes.node`]; | ||
deprecated[`${pragma}.isValidClass`] = ['0.12.0']; | ||
deprecated['this.transferPropsTo'] = ['0.12.0', 'spread operator ({...})']; | ||
// 0.13.0 | ||
deprecated[`${pragma}.addons.classSet`] = ['0.13.0', 'the npm module classnames']; | ||
deprecated[`${pragma}.addons.cloneWithProps`] = ['0.13.0', `${pragma}.cloneElement`]; | ||
// 0.14.0 | ||
deprecated[`${pragma}.render`] = ['0.14.0', 'ReactDOM.render']; | ||
deprecated[`${pragma}.unmountComponentAtNode`] = ['0.14.0', 'ReactDOM.unmountComponentAtNode']; | ||
deprecated[`${pragma}.findDOMNode`] = ['0.14.0', 'ReactDOM.findDOMNode']; | ||
deprecated[`${pragma}.renderToString`] = ['0.14.0', 'ReactDOMServer.renderToString']; | ||
deprecated[`${pragma}.renderToStaticMarkup`] = ['0.14.0', 'ReactDOMServer.renderToStaticMarkup']; | ||
// 15.0.0 | ||
deprecated[`${pragma}.addons.LinkedStateMixin`] = ['15.0.0']; | ||
deprecated['ReactPerf.printDOM'] = ['15.0.0', 'ReactPerf.printOperations']; | ||
deprecated['Perf.printDOM'] = ['15.0.0', 'Perf.printOperations']; | ||
deprecated['ReactPerf.getMeasurementsSummaryMap'] = ['15.0.0', 'ReactPerf.getWasted']; | ||
deprecated['Perf.getMeasurementsSummaryMap'] = ['15.0.0', 'Perf.getWasted']; | ||
// 15.5.0 | ||
deprecated[`${pragma}.createClass`] = ['15.5.0', 'the npm module create-react-class']; | ||
deprecated[`${pragma}.addons.TestUtils`] = ['15.5.0', 'ReactDOM.TestUtils']; | ||
deprecated[`${pragma}.PropTypes`] = ['15.5.0', 'the npm module prop-types']; | ||
// 15.6.0 | ||
deprecated[`${pragma}.DOM`] = ['15.6.0', 'the npm module react-dom-factories']; | ||
// 16.9.0 | ||
// For now the following life-cycle methods are just legacy, not deprecated: | ||
// `componentWillMount`, `componentWillReceiveProps`, `componentWillUpdate` | ||
// https://github.com/yannickcr/eslint-plugin-react/pull/1750#issuecomment-425975934 | ||
deprecated.componentWillMount = [ | ||
'16.9.0', | ||
'UNSAFE_componentWillMount', | ||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount. ' | ||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.', | ||
]; | ||
deprecated.componentWillReceiveProps = [ | ||
'16.9.0', | ||
'UNSAFE_componentWillReceiveProps', | ||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops. ' | ||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.', | ||
]; | ||
deprecated.componentWillUpdate = [ | ||
'16.9.0', | ||
'UNSAFE_componentWillUpdate', | ||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate. ' | ||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.', | ||
]; | ||
return deprecated; | ||
} | ||
const messages = { | ||
@@ -52,65 +108,7 @@ deprecated: '{{oldMethod}} is deprecated since React {{version}}{{newMethod}}{{refs}}', | ||
create: Components.detect((context, components, utils) => { | ||
create(context) { | ||
const pragma = pragmaUtil.getFromContext(context); | ||
const deprecated = getDeprecated(pragma); | ||
function getDeprecated() { | ||
const deprecated = {}; | ||
// 0.12.0 | ||
deprecated[`${pragma}.renderComponent`] = ['0.12.0', `${pragma}.render`]; | ||
deprecated[`${pragma}.renderComponentToString`] = ['0.12.0', `${pragma}.renderToString`]; | ||
deprecated[`${pragma}.renderComponentToStaticMarkup`] = ['0.12.0', `${pragma}.renderToStaticMarkup`]; | ||
deprecated[`${pragma}.isValidComponent`] = ['0.12.0', `${pragma}.isValidElement`]; | ||
deprecated[`${pragma}.PropTypes.component`] = ['0.12.0', `${pragma}.PropTypes.element`]; | ||
deprecated[`${pragma}.PropTypes.renderable`] = ['0.12.0', `${pragma}.PropTypes.node`]; | ||
deprecated[`${pragma}.isValidClass`] = ['0.12.0']; | ||
deprecated['this.transferPropsTo'] = ['0.12.0', 'spread operator ({...})']; | ||
// 0.13.0 | ||
deprecated[`${pragma}.addons.classSet`] = ['0.13.0', 'the npm module classnames']; | ||
deprecated[`${pragma}.addons.cloneWithProps`] = ['0.13.0', `${pragma}.cloneElement`]; | ||
// 0.14.0 | ||
deprecated[`${pragma}.render`] = ['0.14.0', 'ReactDOM.render']; | ||
deprecated[`${pragma}.unmountComponentAtNode`] = ['0.14.0', 'ReactDOM.unmountComponentAtNode']; | ||
deprecated[`${pragma}.findDOMNode`] = ['0.14.0', 'ReactDOM.findDOMNode']; | ||
deprecated[`${pragma}.renderToString`] = ['0.14.0', 'ReactDOMServer.renderToString']; | ||
deprecated[`${pragma}.renderToStaticMarkup`] = ['0.14.0', 'ReactDOMServer.renderToStaticMarkup']; | ||
// 15.0.0 | ||
deprecated[`${pragma}.addons.LinkedStateMixin`] = ['15.0.0']; | ||
deprecated['ReactPerf.printDOM'] = ['15.0.0', 'ReactPerf.printOperations']; | ||
deprecated['Perf.printDOM'] = ['15.0.0', 'Perf.printOperations']; | ||
deprecated['ReactPerf.getMeasurementsSummaryMap'] = ['15.0.0', 'ReactPerf.getWasted']; | ||
deprecated['Perf.getMeasurementsSummaryMap'] = ['15.0.0', 'Perf.getWasted']; | ||
// 15.5.0 | ||
deprecated[`${pragma}.createClass`] = ['15.5.0', 'the npm module create-react-class']; | ||
deprecated[`${pragma}.addons.TestUtils`] = ['15.5.0', 'ReactDOM.TestUtils']; | ||
deprecated[`${pragma}.PropTypes`] = ['15.5.0', 'the npm module prop-types']; | ||
// 15.6.0 | ||
deprecated[`${pragma}.DOM`] = ['15.6.0', 'the npm module react-dom-factories']; | ||
// 16.9.0 | ||
// For now the following life-cycle methods are just legacy, not deprecated: | ||
// `componentWillMount`, `componentWillReceiveProps`, `componentWillUpdate` | ||
// https://github.com/yannickcr/eslint-plugin-react/pull/1750#issuecomment-425975934 | ||
deprecated.componentWillMount = [ | ||
'16.9.0', | ||
'UNSAFE_componentWillMount', | ||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillmount. ' | ||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.', | ||
]; | ||
deprecated.componentWillReceiveProps = [ | ||
'16.9.0', | ||
'UNSAFE_componentWillReceiveProps', | ||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops. ' | ||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.', | ||
]; | ||
deprecated.componentWillUpdate = [ | ||
'16.9.0', | ||
'UNSAFE_componentWillUpdate', | ||
'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate. ' | ||
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.', | ||
]; | ||
return deprecated; | ||
} | ||
function isDeprecated(method) { | ||
const deprecated = getDeprecated(); | ||
return ( | ||
@@ -128,3 +126,2 @@ deprecated | ||
} | ||
const deprecated = getDeprecated(); | ||
const version = deprecated[methodName][0]; | ||
@@ -176,3 +173,6 @@ const newMethod = deprecated[methodName][1]; | ||
function checkLifeCycleMethods(node) { | ||
if (utils.isES5Component(node) || utils.isES6Component(node)) { | ||
if ( | ||
componentUtil.isES5Component(node, context) | ||
|| componentUtil.isES6Component(node, context) | ||
) { | ||
const methods = getLifeCycleMethods(node); | ||
@@ -231,3 +231,3 @@ methods.forEach((method) => checkDeprecation(node, method.name, method.node)); | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -10,2 +10,3 @@ /** | ||
const Components = require('../util/Components'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -103,3 +104,3 @@ const report = require('../util/report'); | ||
const item = getOuterMemberExpression(node.left); | ||
if (utils.isStateMemberExpression(item)) { | ||
if (componentUtil.isStateMemberExpression(item)) { | ||
const mutations = (component && component.mutations) || []; | ||
@@ -120,3 +121,3 @@ mutations.push(node.left.object); | ||
const item = getOuterMemberExpression(node.argument); | ||
if (utils.isStateMemberExpression(item)) { | ||
if (componentUtil.isStateMemberExpression(item)) { | ||
const mutations = (component && component.mutations) || []; | ||
@@ -123,0 +124,0 @@ mutations.push(item); |
@@ -7,4 +7,4 @@ /** | ||
const Components = require('../util/Components'); | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -35,3 +35,3 @@ const report = require('../util/report'); | ||
create: Components.detect((context, components, utils) => { | ||
create(context) { | ||
/** | ||
@@ -70,3 +70,3 @@ * Checks for shouldComponentUpdate property | ||
function checkForViolation(node) { | ||
if (utils.isPureComponent(node)) { | ||
if (componentUtil.isPureComponent(node, context)) { | ||
const hasScu = hasShouldComponentUpdate(node); | ||
@@ -89,3 +89,3 @@ if (hasScu) { | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -8,3 +8,3 @@ /** | ||
const Components = require('../util/Components'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -44,3 +44,3 @@ const report = require('../util/report'); | ||
create: Components.detect((context, components, utils) => { | ||
create(context) { | ||
const detectTemplateLiterals = context.options[0] ? context.options[0].noTemplateLiterals : false; | ||
@@ -54,3 +54,3 @@ /** | ||
return !!( | ||
(utils.getParentES6Component() || utils.getParentES5Component()) | ||
(componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context)) | ||
&& node.object.type === 'ThisExpression' | ||
@@ -121,3 +121,3 @@ && node.property.name === 'refs' | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -10,2 +10,3 @@ /** | ||
const docsUrl = require('../util/docsUrl'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const report = require('../util/report'); | ||
@@ -206,3 +207,3 @@ const lifecycleMethods = require('../util/lifecycleMethods'); | ||
'ClassProperty, PropertyDefinition'(node) { | ||
if (!node.static || !utils.isES6Component(node.parent.parent)) { | ||
if (!node.static || !componentUtil.isES6Component(node.parent.parent, context)) { | ||
return; | ||
@@ -228,3 +229,3 @@ } | ||
relatedComponent | ||
&& (utils.isES6Component(relatedComponent.node) || ( | ||
&& (componentUtil.isES6Component(relatedComponent.node, context) || ( | ||
relatedComponent.node.type !== 'ClassDeclaration' && utils.isReturningJSX(relatedComponent.node))) | ||
@@ -238,3 +239,3 @@ && (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right) | ||
MethodDefinition(node) { | ||
if (!utils.isES6Component(node.parent.parent)) { | ||
if (!componentUtil.isES6Component(node.parent.parent, context)) { | ||
return; | ||
@@ -247,3 +248,3 @@ } | ||
ObjectExpression(node) { | ||
const component = utils.isES5Component(node) && components.get(node); | ||
const component = componentUtil.isES5Component(node, context) && components.get(node); | ||
@@ -250,0 +251,0 @@ if (!component) { |
@@ -30,3 +30,4 @@ /** | ||
const ATTRIBUTE_TAGS_MAP = { | ||
crossOrigin: ['script', 'img', 'video', 'audio', 'link'], | ||
// image is required for SVG support, all other tags are HTML. | ||
crossOrigin: ['script', 'img', 'video', 'audio', 'link', 'image'], | ||
}; | ||
@@ -33,0 +34,0 @@ |
@@ -8,4 +8,4 @@ /** | ||
const Components = require('../util/Components'); | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -48,3 +48,3 @@ const testReactVersion = require('../util/version').testReactVersion; | ||
create: Components.detect((context, components, utils) => { | ||
create(context) { | ||
const config = context.options[0] || {}; | ||
@@ -138,3 +138,3 @@ const checkAliases = config.checkAliases || false; | ||
function checkLifeCycleMethods(node) { | ||
if (utils.isES5Component(node) || utils.isES6Component(node)) { | ||
if (componentUtil.isES5Component(node, context) || componentUtil.isES6Component(node, context)) { | ||
const methods = getLifeCycleMethods(node); | ||
@@ -150,3 +150,3 @@ methods.forEach((method) => checkUnsafe(node, method)); | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -17,3 +17,2 @@ /** | ||
const ERROR_MESSAGE_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.'; | ||
const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.'; | ||
@@ -28,7 +27,7 @@ const HOOK_REGEXP = /^use[A-Z0-9].*$/; | ||
* Generate error message with given parent component name | ||
* @param {String} parentName Name of the parent component | ||
* @param {String} parentName Name of the parent component, if known | ||
* @returns {String} Error message with parent component name | ||
*/ | ||
function generateErrorMessageWithParentName(parentName) { | ||
return `Declare this component outside parent component "${parentName}" or memoize it.`; | ||
return `Do not define components during render. React will see a new component type on every render and destroy the entire subtree’s DOM nodes and state (https://reactjs.org/docs/reconciliation.html#elements-of-different-types). Instead, move this component definition out of the parent component${parentName ? ` “${parentName}” ` : ' '}and pass data as props.`; | ||
} | ||
@@ -469,5 +468,3 @@ | ||
let message = parentName | ||
? generateErrorMessageWithParentName(parentName) | ||
: ERROR_MESSAGE_WITHOUT_NAME; | ||
let message = generateErrorMessageWithParentName(parentName); | ||
@@ -494,4 +491,5 @@ // Add information about allowAsProps option when component is declared inside prop | ||
ClassDeclaration(node) { validate(node); }, | ||
CallExpression(node) { validate(node); }, | ||
}; | ||
}), | ||
}; |
@@ -8,4 +8,4 @@ /** | ||
const Components = require('../util/Components'); | ||
const docsUrl = require('../util/docsUrl'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const report = require('../util/report'); | ||
@@ -119,3 +119,3 @@ | ||
create: Components.detect((context, components, utils) => { | ||
create: ((context) => { | ||
let classInfo = null; | ||
@@ -176,3 +176,3 @@ | ||
ClassDeclaration(node) { | ||
if (utils.isES6Component(node)) { | ||
if (componentUtil.isES6Component(node, context)) { | ||
classInfo = getInitialClassInfo(node, true); | ||
@@ -183,3 +183,3 @@ } | ||
ObjectExpression(node) { | ||
if (utils.isES5Component(node)) { | ||
if (componentUtil.isES5Component(node, context)) { | ||
classInfo = getInitialClassInfo(node, false); | ||
@@ -186,0 +186,0 @@ } |
@@ -9,3 +9,3 @@ /** | ||
// As for exceptions for props.children or props.className (and alike) look at | ||
// https://github.com/yannickcr/eslint-plugin-react/issues/7 | ||
// https://github.com/jsx-eslint/eslint-plugin-react/issues/7 | ||
@@ -12,0 +12,0 @@ const Components = require('../util/Components'); |
@@ -12,5 +12,5 @@ /** | ||
const Components = require('../util/Components'); | ||
const docsUrl = require('../util/docsUrl'); | ||
const ast = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const report = require('../util/report'); | ||
@@ -95,3 +95,3 @@ | ||
create: Components.detect((context, components, utils) => { | ||
create(context) { | ||
// Non-null when we are inside a React component ClassDeclaration and we have | ||
@@ -166,2 +166,5 @@ // not yet encountered any use of this.state which we have chosen not to | ||
function addUsedStateField(node) { | ||
if (!classInfo) { | ||
return; | ||
} | ||
const name = getName(node); | ||
@@ -236,3 +239,3 @@ if (name) { | ||
function handleES6ComponentEnter(node) { | ||
if (utils.isES6Component(node)) { | ||
if (componentUtil.isES6Component(node, context)) { | ||
classInfo = getInitialClassInfo(); | ||
@@ -274,3 +277,3 @@ } | ||
ObjectExpression(node) { | ||
if (utils.isES5Component(node)) { | ||
if (componentUtil.isES5Component(node, context)) { | ||
classInfo = getInitialClassInfo(); | ||
@@ -285,3 +288,3 @@ } | ||
if (utils.isES5Component(node)) { | ||
if (componentUtil.isES5Component(node, context)) { | ||
reportUnusedFields(); | ||
@@ -380,3 +383,3 @@ classInfo = null; | ||
const stateArg = node.value.params[1]; // probably "state" | ||
if (!scope.variables) { | ||
if (!scope || !scope.variables) { | ||
return; | ||
@@ -431,3 +434,3 @@ } | ||
const parent = node.parent; | ||
if (!utils.isES5Component(parent.parent)) { | ||
if (!componentUtil.isES5Component(parent.parent, context)) { | ||
return; | ||
@@ -524,3 +527,3 @@ } | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -8,3 +8,3 @@ /** | ||
const Components = require('../util/Components'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -38,3 +38,3 @@ const report = require('../util/report'); | ||
create: Components.detect((context, components, utils) => { | ||
create(context) { | ||
const configuration = context.options[0] || 'always'; | ||
@@ -44,3 +44,3 @@ | ||
ObjectExpression(node) { | ||
if (utils.isES5Component(node) && configuration === 'always') { | ||
if (componentUtil.isES5Component(node, context) && configuration === 'always') { | ||
report(context, messages.shouldUseES6Class, 'shouldUseES6Class', { | ||
@@ -52,3 +52,3 @@ node, | ||
ClassDeclaration(node) { | ||
if (utils.isES6Component(node) && configuration === 'never') { | ||
if (componentUtil.isES6Component(node, context) && configuration === 'never') { | ||
report(context, messages.shouldUseCreateClass, 'shouldUseCreateClass', { | ||
@@ -60,3 +60,3 @@ node, | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -13,2 +13,3 @@ /** | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -276,3 +277,3 @@ const report = require('../util/report'); | ||
function visitClass(node) { | ||
if (ignorePureComponents && utils.isPureComponent(node)) { | ||
if (ignorePureComponents && componentUtil.isPureComponent(node, context)) { | ||
markSCUAsDeclared(node); | ||
@@ -375,3 +376,6 @@ } | ||
|| list[component].useDecorators | ||
|| (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) | ||
|| ( | ||
!componentUtil.isES5Component(list[component].node, context) | ||
&& !componentUtil.isES6Component(list[component].node, context) | ||
) | ||
) { | ||
@@ -378,0 +382,0 @@ return; |
@@ -9,3 +9,3 @@ /** | ||
// As for exceptions for props.children or props.className (and alike) look at | ||
// https://github.com/yannickcr/eslint-plugin-react/issues/7 | ||
// https://github.com/jsx-eslint/eslint-plugin-react/issues/7 | ||
@@ -12,0 +12,0 @@ const Components = require('../util/Components'); |
@@ -8,2 +8,4 @@ /** | ||
const entries = require('object.entries'); | ||
const values = require('object.values'); | ||
const Components = require('../util/Components'); | ||
@@ -21,2 +23,5 @@ const docsUrl = require('../util/docsUrl'); | ||
shouldHaveDefault: 'propType "{{name}}" is not required, but has no corresponding defaultProps declaration.', | ||
noDefaultPropsWithFunction: 'Don’t use defaultProps with function components.', | ||
shouldAssignObjectDefault: 'propType "{{name}}" is not required, but has no corresponding default argument value.', | ||
destructureInSignature: 'Must destructure props in the function signature to initialize an optional prop.', | ||
}; | ||
@@ -40,2 +45,15 @@ | ||
}, | ||
classes: { | ||
allow: { | ||
enum: ['defaultProps', 'ignore'], | ||
}, | ||
}, | ||
functions: { | ||
allow: { | ||
enum: ['defaultArguments', 'defaultProps', 'ignore'], | ||
}, | ||
}, | ||
/** | ||
* @deprecated | ||
*/ | ||
ignoreFunctionalComponents: { | ||
@@ -52,3 +70,11 @@ type: 'boolean', | ||
const forbidDefaultForRequired = configuration.forbidDefaultForRequired || false; | ||
const ignoreFunctionalComponents = configuration.ignoreFunctionalComponents || false; | ||
const classes = configuration.classes || 'defaultProps'; | ||
/** | ||
* @todo | ||
* - Remove ignoreFunctionalComponents | ||
* - Change default to 'defaultArguments' | ||
*/ | ||
const functions = configuration.ignoreFunctionalComponents | ||
? 'ignore' | ||
: configuration.functions || 'defaultProps'; | ||
@@ -62,10 +88,6 @@ /** | ||
function reportPropTypesWithoutDefault(propTypes, defaultProps) { | ||
// If this defaultProps is "unresolved", then we should ignore this component and not report | ||
// any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators. | ||
if (defaultProps === 'unresolved') { | ||
return; | ||
} | ||
entries(propTypes).forEach((propType) => { | ||
const propName = propType[0]; | ||
const prop = propType[1]; | ||
Object.keys(propTypes).forEach((propName) => { | ||
const prop = propTypes[propName]; | ||
if (!prop.node) { | ||
@@ -95,2 +117,44 @@ return; | ||
/** | ||
* If functions option is 'defaultArguments', reports defaultProps is used and all params that doesn't initialized. | ||
* @param {Object} componentNode Node of component. | ||
* @param {Object[]} declaredPropTypes List of propTypes to check `isRequired`. | ||
* @param {Object} defaultProps Object of defaultProps to check used. | ||
*/ | ||
function reportFunctionComponent(componentNode, declaredPropTypes, defaultProps) { | ||
if (defaultProps) { | ||
report(context, messages.noDefaultPropsWithFunction, 'noDefaultPropsWithFunction', { | ||
node: componentNode, | ||
}); | ||
} | ||
const props = componentNode.params[0]; | ||
const propTypes = declaredPropTypes; | ||
if (props.type === 'Identifier') { | ||
const hasOptionalProp = values(propTypes).some((propType) => !propType.isRequired); | ||
if (hasOptionalProp) { | ||
report(context, messages.destructureInSignature, 'destructureInSignature', { | ||
node: props, | ||
}); | ||
} | ||
} else if (props.type === 'ObjectPattern') { | ||
props.properties.filter((prop) => { | ||
if (prop.type === 'RestElement' || prop.type === 'ExperimentalRestProperty') { | ||
return false; | ||
} | ||
const propType = propTypes[prop.key.name]; | ||
if (!propType || propType.isRequired) { | ||
return false; | ||
} | ||
return prop.value.type !== 'AssignmentPattern'; | ||
}).forEach((prop) => { | ||
report(context, messages.shouldAssignObjectDefault, 'shouldAssignObjectDefault', { | ||
node: prop, | ||
data: { name: prop.key.name }, | ||
}); | ||
}); | ||
} | ||
} | ||
// -------------------------------------------------------------------------- | ||
@@ -104,13 +168,29 @@ // Public API | ||
Object.keys(list).filter((component) => { | ||
if (ignoreFunctionalComponents | ||
&& (astUtil.isFunction(list[component].node) || astUtil.isFunctionLikeExpression(list[component].node))) { | ||
values(list).filter((component) => { | ||
if (functions === 'ignore' && astUtil.isFunctionLike(component.node)) { | ||
return false; | ||
} | ||
return list[component].declaredPropTypes; | ||
if (classes === 'ignore' && astUtil.isClass(component.node)) { | ||
return false; | ||
} | ||
// If this defaultProps is "unresolved", then we should ignore this component and not report | ||
// any errors for it, to avoid false-positives with e.g. external defaultProps declarations or spread operators. | ||
if (component.defaultProps === 'unresolved') { | ||
return false; | ||
} | ||
return component.declaredPropTypes !== undefined; | ||
}).forEach((component) => { | ||
reportPropTypesWithoutDefault( | ||
list[component].declaredPropTypes, | ||
list[component].defaultProps || {} | ||
); | ||
if (functions === 'defaultArguments' && astUtil.isFunctionLike(component.node)) { | ||
reportFunctionComponent( | ||
component.node, | ||
component.declaredPropTypes, | ||
component.defaultProps | ||
); | ||
} else { | ||
reportPropTypesWithoutDefault( | ||
component.declaredPropTypes, | ||
component.defaultProps || {} | ||
); | ||
} | ||
}); | ||
@@ -117,0 +197,0 @@ }, |
@@ -9,2 +9,3 @@ /** | ||
const Components = require('../util/Components'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -42,3 +43,3 @@ const report = require('../util/report'); | ||
create: Components.detect((context, components, utils) => { | ||
create: Components.detect((context, components) => { | ||
const configuration = context.options[0] || {}; | ||
@@ -182,3 +183,7 @@ const allowDecorators = configuration.allowDecorators || []; | ||
ClassDeclaration(node) { | ||
if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) { | ||
if (!( | ||
hasPureRenderDecorator(node) | ||
|| hasCustomDecorator(node) | ||
|| componentUtil.isPureComponent(node, context) | ||
)) { | ||
return; | ||
@@ -185,0 +190,0 @@ } |
@@ -10,2 +10,3 @@ /** | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -36,3 +37,3 @@ const report = require('../util/report'); | ||
create: Components.detect((context, components, utils) => { | ||
create: Components.detect((context, components) => { | ||
/** | ||
@@ -66,3 +67,3 @@ * Mark a return statement as present | ||
if (/Function(Expression|Declaration)$/.test(ancestor.type)) { | ||
depth++; | ||
depth += 1; | ||
} | ||
@@ -92,3 +93,6 @@ if ( | ||
|| list[component].hasReturnStatement | ||
|| (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) | ||
|| ( | ||
!componentUtil.isES5Component(list[component].node, context) | ||
&& !componentUtil.isES6Component(list[component].node, context) | ||
) | ||
) { | ||
@@ -95,0 +99,0 @@ return; |
@@ -269,3 +269,3 @@ /** | ||
// Increment the prop score | ||
errors[propA.index].score++; | ||
errors[propA.index].score += 1; | ||
// Stop here if we already have pushed another node at this position | ||
@@ -272,0 +272,0 @@ if (getPropertyName(errors[propA.index].node) !== getPropertyName(propA.node)) { |
@@ -8,3 +8,4 @@ /** | ||
const Components = require('../util/Components'); | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const docsUrl = require('../util/docsUrl'); | ||
@@ -38,3 +39,3 @@ const report = require('../util/report'); | ||
create: Components.detect((context, components, utils) => { | ||
create(context) { | ||
const option = context.options[0] || 'always'; | ||
@@ -47,3 +48,3 @@ return { | ||
&& node.key.name === 'state' | ||
&& utils.getParentES6Component() | ||
&& componentUtil.getParentES6Component(context) | ||
) { | ||
@@ -58,5 +59,5 @@ report(context, messages.stateInitConstructor, 'stateInitConstructor', { | ||
option === 'never' | ||
&& utils.isStateMemberExpression(node.left) | ||
&& utils.inConstructor() | ||
&& utils.getParentES6Component() | ||
&& componentUtil.isStateMemberExpression(node.left) | ||
&& astUtil.inConstructor(context) | ||
&& componentUtil.getParentES6Component(context) | ||
) { | ||
@@ -69,3 +70,3 @@ report(context, messages.stateInitClassProp, 'stateInitClassProp', { | ||
}; | ||
}), | ||
}, | ||
}; |
@@ -12,2 +12,3 @@ /** | ||
const astUtil = require('../util/ast'); | ||
const componentUtil = require('../util/componentUtil'); | ||
const propsUtil = require('../util/props'); | ||
@@ -145,3 +146,3 @@ const report = require('../util/report'); | ||
'ClassProperty, PropertyDefinition'(node) { | ||
if (!utils.getParentES6Component()) { | ||
if (!componentUtil.getParentES6Component(context)) { | ||
return; | ||
@@ -165,3 +166,3 @@ } | ||
// If the related component is not an ES6 component then skip this node | ||
if (!relatedComponent || !utils.isES6Component(relatedComponent.node)) { | ||
if (!relatedComponent || !componentUtil.isES6Component(relatedComponent.node, context)) { | ||
return; | ||
@@ -176,3 +177,3 @@ } | ||
// If the function is inside a class and is static getter then check if correctly positioned | ||
if (utils.getParentES6Component() && node.static && node.kind === 'get') { | ||
if (componentUtil.getParentES6Component(context) && node.static && node.kind === 'get') { | ||
// Report error if needed | ||
@@ -179,0 +180,0 @@ reportNodeIncorrectlyPositioned(node, STATIC_GETTER); |
@@ -11,4 +11,4 @@ /** | ||
const Components = require('../util/Components'); | ||
const docsUrl = require('../util/docsUrl'); | ||
const isCreateElement = require('../util/isCreateElement'); | ||
const report = require('../util/report'); | ||
@@ -67,3 +67,3 @@ | ||
create: Components.detect((context, components, utils) => ({ | ||
create: (context) => ({ | ||
JSXElement(node) { | ||
@@ -113,3 +113,3 @@ const elementName = node.openingElement.name.name; | ||
if (!utils.isCreateElement(node)) { | ||
if (!isCreateElement(node, context)) { | ||
return; | ||
@@ -167,3 +167,3 @@ } | ||
}, | ||
})), | ||
}), | ||
}; |
@@ -16,5 +16,3 @@ import eslint from 'eslint'; | ||
interface Context extends eslint.SourceCode { | ||
getFirstTokens(node: estree.Node | ASTNode, options?: eslint.SourceCode.CursorWithCountOptions): eslint.AST.Token[]; | ||
} | ||
type Context = eslint.Rule.RuleContext | ||
@@ -21,0 +19,0 @@ type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set<typeof annotation>) => object; |
@@ -31,2 +31,17 @@ /** | ||
function loopNodes(nodes) { | ||
for (let i = nodes.length - 1; i >= 0; i--) { | ||
if (nodes[i].type === 'ReturnStatement') { | ||
return nodes[i]; | ||
} | ||
if (nodes[i].type === 'SwitchStatement') { | ||
const j = nodes[i].cases.length - 1; | ||
if (j >= 0) { | ||
return loopNodes(nodes[i].cases[j].consequent); | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
/** | ||
@@ -46,21 +61,8 @@ * Find a return statment in the current node | ||
const bodyNodes = (node.value ? node.value.body.body : node.body.body); | ||
const bodyNodes = node.value ? node.value.body.body : node.body.body; | ||
return (function loopNodes(nodes) { | ||
let i = nodes.length - 1; | ||
for (; i >= 0; i--) { | ||
if (nodes[i].type === 'ReturnStatement') { | ||
return nodes[i]; | ||
} | ||
if (nodes[i].type === 'SwitchStatement') { | ||
let j = nodes[i].cases.length - 1; | ||
for (; j >= 0; j--) { | ||
return loopNodes(nodes[i].cases[j].consequent); | ||
} | ||
} | ||
} | ||
return false; | ||
}(bodyNodes)); | ||
return loopNodes(bodyNodes); | ||
} | ||
// eslint-disable-next-line valid-jsdoc -- valid-jsdoc cannot parse function types. | ||
/** | ||
@@ -72,14 +74,17 @@ * Helper function for traversing "returns" (return statements or the | ||
* @param {Context} context The context of `ASTNode`. | ||
* @param {function} enterFunc Function to execute for each returnStatement found | ||
* @param {(returnValue: ASTNode, breakTraverse: () => void) => void} onReturn | ||
* Function to execute for each returnStatement found | ||
* @returns {undefined} | ||
*/ | ||
function traverseReturns(ASTNode, context, enterFunc) { | ||
function traverseReturns(ASTNode, context, onReturn) { | ||
const nodeType = ASTNode.type; | ||
if (nodeType === 'ReturnStatement') { | ||
return enterFunc(ASTNode); | ||
onReturn(ASTNode.argument, () => {}); | ||
return; | ||
} | ||
if (nodeType === 'ArrowFunctionExpression' && ASTNode.expression) { | ||
return enterFunc(ASTNode.body); | ||
onReturn(ASTNode.body, () => {}); | ||
return; | ||
} | ||
@@ -116,11 +121,19 @@ | ||
enter(node) { | ||
const breakTraverse = () => { | ||
this.break(); | ||
}; | ||
switch (node.type) { | ||
case 'ReturnStatement': | ||
this.skip(); | ||
return enterFunc(node); | ||
case 'FunctionExpression': | ||
case 'FunctionDeclaration': | ||
case 'ArrowFunctionExpression': | ||
return this.skip(); | ||
onReturn(node.argument, breakTraverse); | ||
return; | ||
case 'BlockStatement': | ||
case 'IfStatement': | ||
case 'ForStatement': | ||
case 'WhileStatement': | ||
case 'SwitchStatement': | ||
case 'SwitchCase': | ||
return; | ||
default: | ||
this.skip(); | ||
} | ||
@@ -227,2 +240,11 @@ }, | ||
/** | ||
* Checks if node is a function declaration or expression or arrow function. | ||
* @param {ASTNode} node The node to check | ||
* @return {Boolean} true if it's a function-like | ||
*/ | ||
function isFunctionLike(node) { | ||
return node.type === 'FunctionDeclaration' || isFunctionLikeExpression(node); | ||
} | ||
/** | ||
* Checks if the node is a class. | ||
@@ -237,2 +259,19 @@ * @param {ASTNode} node The node to check | ||
/** | ||
* Check if we are in a class constructor | ||
* @param {Context} context | ||
* @return {boolean} | ||
*/ | ||
function inConstructor(context) { | ||
let scope = context.getScope(); | ||
while (scope) { | ||
// @ts-ignore | ||
if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { | ||
return true; | ||
} | ||
scope = scope.upper; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Removes quotes from around an identifier. | ||
@@ -249,3 +288,3 @@ * @param {string} string the identifier to strip | ||
* @param {Context} context The AST node with the key. | ||
* @param {ASTNode} node The AST node with the key. | ||
* @param {any} node The AST node with the key. | ||
* @return {string | undefined} the name of the key | ||
@@ -255,3 +294,3 @@ */ | ||
if (node.type === 'ObjectTypeProperty') { | ||
const tokens = context.getFirstTokens(node, 2); | ||
const tokens = context.getSourceCode().getFirstTokens(node, 2); | ||
return (tokens[0].value === '+' || tokens[0].value === '-' | ||
@@ -276,2 +315,19 @@ ? tokens[1].value | ||
/** | ||
* Checks if a node is surrounded by parenthesis. | ||
* | ||
* @param {object} context - Context from the rule | ||
* @param {ASTNode} node - Node to be checked | ||
* @returns {boolean} | ||
*/ | ||
function isParenthesized(context, node) { | ||
const sourceCode = context.getSourceCode(); | ||
const previousToken = sourceCode.getTokenBefore(node); | ||
const nextToken = sourceCode.getTokenAfter(node); | ||
return !!previousToken && !!nextToken | ||
&& previousToken.value === '(' && previousToken.range[1] <= node.range[0] | ||
&& nextToken.value === ')' && nextToken.range[0] >= node.range[1]; | ||
} | ||
/** | ||
* Checks if a node is being assigned a value: props.bar = 'bar' | ||
@@ -392,2 +448,3 @@ * @param {ASTNode} node The AST node being checked. | ||
getKeyValue, | ||
isParenthesized, | ||
isAssignmentLHS, | ||
@@ -397,2 +454,4 @@ isClass, | ||
isFunctionLikeExpression, | ||
isFunctionLike, | ||
inConstructor, | ||
isNodeFirstInLine, | ||
@@ -399,0 +458,0 @@ unwrapTSAsExpression, |
@@ -8,3 +8,2 @@ /** | ||
const doctrine = require('doctrine'); | ||
const arrayIncludes = require('array-includes'); | ||
@@ -17,2 +16,3 @@ const fromEntries = require('object.fromentries'); | ||
const astUtil = require('./ast'); | ||
const componentUtil = require('./componentUtil'); | ||
const propTypesUtil = require('./propTypes'); | ||
@@ -23,3 +23,2 @@ const jsxUtil = require('./jsx'); | ||
const isFirstLetterCapitalized = require('./isFirstLetterCapitalized'); | ||
const isCreateElement = require('./isCreateElement'); | ||
const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport'); | ||
@@ -254,4 +253,36 @@ | ||
// eslint-disable-next-line valid-jsdoc | ||
/** | ||
* Merge many eslint rules into one | ||
* @param {{[_: string]: Function}[]} rules the returned values for eslint rule.create(context) | ||
* @returns {{[_: string]: Function}} merged rule | ||
*/ | ||
function mergeRules(rules) { | ||
/** @type {Map<string, Function[]>} */ | ||
const handlersByKey = new Map(); | ||
rules.forEach((rule) => { | ||
Object.keys(rule).forEach((key) => { | ||
const fns = handlersByKey.get(key); | ||
if (!fns) { | ||
handlersByKey.set(key, [rule[key]]); | ||
} else { | ||
fns.push(rule[key]); | ||
} | ||
}); | ||
}); | ||
/** @type {{[key: string]: Function}} */ | ||
const rule = {}; | ||
handlersByKey.forEach((fns, key) => { | ||
rule[key] = function mergedHandler(node) { | ||
fns.forEach((fn) => { | ||
fn(node); | ||
}); | ||
}; | ||
}); | ||
return rule; | ||
} | ||
function componentRule(rule, context) { | ||
const createClass = pragmaUtil.getCreateClassFromContext(context); | ||
const pragma = pragmaUtil.getFromContext(context); | ||
@@ -264,85 +295,3 @@ const sourceCode = context.getSourceCode(); | ||
const utils = { | ||
/** | ||
* Check if the node is a React ES5 component | ||
* | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Boolean} True if the node is a React ES5 component, false if not | ||
*/ | ||
isES5Component(node) { | ||
if (!node.parent) { | ||
return false; | ||
} | ||
return new RegExp(`^(${pragma}\\.)?${createClass}$`).test(sourceCode.getText(node.parent.callee)); | ||
}, | ||
/** | ||
* Check if the node is a React ES6 component | ||
* | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Boolean} True if the node is a React ES6 component, false if not | ||
*/ | ||
isES6Component(node) { | ||
if (utils.isExplicitComponent(node)) { | ||
return true; | ||
} | ||
if (!node.superClass) { | ||
return false; | ||
} | ||
return new RegExp(`^(${pragma}\\.)?(Pure)?Component$`).test(sourceCode.getText(node.superClass)); | ||
}, | ||
/** | ||
* Check if the node is explicitly declared as a descendant of a React Component | ||
* | ||
* @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression). | ||
* @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not | ||
*/ | ||
isExplicitComponent(node) { | ||
let comment; | ||
// Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes. | ||
// Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27 | ||
// eslint-disable-next-line no-warning-comments | ||
// FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented. | ||
try { | ||
comment = sourceCode.getJSDocComment(node); | ||
} catch (e) { | ||
comment = null; | ||
} | ||
if (comment === null) { | ||
return false; | ||
} | ||
let commentAst; | ||
try { | ||
commentAst = doctrine.parse(comment.value, { | ||
unwrap: true, | ||
tags: ['extends', 'augments'], | ||
}); | ||
} catch (e) { | ||
// handle a bug in the archived `doctrine`, see #2596 | ||
return false; | ||
} | ||
const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent'); | ||
return relevantTags.length > 0; | ||
}, | ||
/** | ||
* Checks to see if our component extends React.PureComponent | ||
* | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Boolean} True if node extends React.PureComponent, false if not | ||
*/ | ||
isPureComponent(node) { | ||
if (node.superClass) { | ||
return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass)); | ||
} | ||
return false; | ||
}, | ||
/** | ||
* Check if variable is destructured from pragma import | ||
@@ -357,46 +306,12 @@ * | ||
/** | ||
* Checks to see if node is called within createElement from pragma | ||
* | ||
* @param {ASTNode} node The AST node being checked. | ||
* @returns {Boolean} True if createElement called from pragma | ||
*/ | ||
isCreateElement(node) { | ||
return isCreateElement(node, context); | ||
}, | ||
/** | ||
* Check if we are in a class constructor | ||
* @return {boolean} true if we are in a class constructor, false if not | ||
*/ | ||
inConstructor() { | ||
let scope = context.getScope(); | ||
while (scope) { | ||
if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { | ||
return true; | ||
} | ||
scope = scope.upper; | ||
} | ||
return false; | ||
}, | ||
/** | ||
* Determine if the node is MemberExpression of `this.state` | ||
* @param {Object} node The node to process | ||
* @returns {Boolean} | ||
*/ | ||
isStateMemberExpression(node) { | ||
return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state'; | ||
}, | ||
isReturningJSX(ASTNode, strict) { | ||
return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict, true); | ||
return jsxUtil.isReturningJSX(ASTNode, context, strict, true); | ||
}, | ||
isReturningJSXOrNull(ASTNode, strict) { | ||
return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict); | ||
return jsxUtil.isReturningJSX(ASTNode, context, strict); | ||
}, | ||
isReturningOnlyNull(ASTNode) { | ||
return jsxUtil.isReturningOnlyNull(this.isCreateElement.bind(this), ASTNode, context); | ||
return jsxUtil.isReturningOnlyNull(ASTNode, context); | ||
}, | ||
@@ -525,4 +440,4 @@ | ||
return ( | ||
utils.getParentES6Component() | ||
|| utils.getParentES5Component() | ||
componentUtil.getParentES6Component(context) | ||
|| componentUtil.getParentES5Component(context) | ||
|| utils.getParentStatelessComponent() | ||
@@ -533,36 +448,2 @@ ); | ||
/** | ||
* Get the parent ES5 component node from the current scope | ||
* | ||
* @returns {ASTNode} component node, null if we are not in a component | ||
*/ | ||
getParentES5Component() { | ||
let scope = context.getScope(); | ||
while (scope) { | ||
const node = scope.block && scope.block.parent && scope.block.parent.parent; | ||
if (node && utils.isES5Component(node)) { | ||
return node; | ||
} | ||
scope = scope.upper; | ||
} | ||
return null; | ||
}, | ||
/** | ||
* Get the parent ES6 component node from the current scope | ||
* | ||
* @returns {ASTNode} component node, null if we are not in a component | ||
*/ | ||
getParentES6Component() { | ||
let scope = context.getScope(); | ||
while (scope && scope.type !== 'class') { | ||
scope = scope.upper; | ||
} | ||
const node = scope && scope.block; | ||
if (!node || !utils.isES6Component(node)) { | ||
return null; | ||
} | ||
return node; | ||
}, | ||
/** | ||
* @param {ASTNode} node | ||
@@ -899,3 +780,3 @@ * @returns {boolean} | ||
ClassExpression(node) { | ||
if (!utils.isES6Component(node)) { | ||
if (!componentUtil.isES6Component(node, context)) { | ||
return; | ||
@@ -907,3 +788,3 @@ } | ||
ClassDeclaration(node) { | ||
if (!utils.isES6Component(node)) { | ||
if (!componentUtil.isES6Component(node, context)) { | ||
return; | ||
@@ -914,12 +795,4 @@ } | ||
'ClassProperty, PropertyDefinition'(node) { | ||
node = utils.getParentComponent(); | ||
if (!node) { | ||
return; | ||
} | ||
components.add(node, 2); | ||
}, | ||
ObjectExpression(node) { | ||
if (!utils.isES5Component(node)) { | ||
if (!componentUtil.isES5Component(node, context)) { | ||
return; | ||
@@ -945,3 +818,3 @@ } | ||
} | ||
components.add(component, 1); | ||
components.add(component, 2); | ||
}, | ||
@@ -959,3 +832,3 @@ | ||
} | ||
components.add(node, 1); | ||
components.add(node, 2); | ||
}, | ||
@@ -978,11 +851,7 @@ | ||
} | ||
if (component.expression && utils.isReturningJSX(component)) { | ||
components.add(component, 2); | ||
} else { | ||
components.add(component, 1); | ||
} | ||
components.add(component, 2); | ||
}, | ||
ThisExpression(node) { | ||
const component = utils.getParentComponent(); | ||
const component = utils.getParentStatelessComponent(); | ||
if (!component || !/Function/.test(component.type) || !node.parent.property) { | ||
@@ -994,15 +863,2 @@ return; | ||
}, | ||
ReturnStatement(node) { | ||
if (!utils.isReturningJSX(node)) { | ||
return; | ||
} | ||
node = utils.getParentComponent(); | ||
if (!node) { | ||
const scope = context.getScope(); | ||
components.add(scope.block, 1); | ||
return; | ||
} | ||
components.add(node, 2); | ||
}, | ||
}; | ||
@@ -1029,40 +885,17 @@ | ||
// Update the provided rule instructions to add the component detection | ||
const ruleInstructions = rule(context, components, utils); | ||
const updatedRuleInstructions = Object.assign({}, ruleInstructions); | ||
const propTypesInstructions = propTypesUtil(context, components, utils); | ||
const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils); | ||
const defaultPropsInstructions = defaultPropsUtil(context, components, utils); | ||
const allKeys = new Set(Object.keys(detectionInstructions).concat( | ||
Object.keys(propTypesInstructions), | ||
Object.keys(usedPropTypesInstructions), | ||
Object.keys(defaultPropsInstructions), | ||
Object.keys(reactImportInstructions) | ||
)); | ||
allKeys.forEach((instruction) => { | ||
updatedRuleInstructions[instruction] = (node) => { | ||
if (instruction in detectionInstructions) { | ||
detectionInstructions[instruction](node); | ||
} | ||
if (instruction in propTypesInstructions) { | ||
propTypesInstructions[instruction](node); | ||
} | ||
if (instruction in usedPropTypesInstructions) { | ||
usedPropTypesInstructions[instruction](node); | ||
} | ||
if (instruction in defaultPropsInstructions) { | ||
defaultPropsInstructions[instruction](node); | ||
} | ||
if (instruction in reactImportInstructions) { | ||
reactImportInstructions[instruction](node); | ||
} | ||
if (ruleInstructions[instruction]) { | ||
return ruleInstructions[instruction](node); | ||
} | ||
}; | ||
}); | ||
const mergedRule = mergeRules([ | ||
detectionInstructions, | ||
propTypesInstructions, | ||
usedPropTypesInstructions, | ||
defaultPropsInstructions, | ||
reactImportInstructions, | ||
ruleInstructions, | ||
]); | ||
// Return the updated rule instructions | ||
return updatedRuleInstructions; | ||
return mergedRule; | ||
} | ||
@@ -1069,0 +902,0 @@ |
@@ -9,2 +9,3 @@ /** | ||
const astUtil = require('./ast'); | ||
const componentUtil = require('./componentUtil'); | ||
const propsUtil = require('./props'); | ||
@@ -175,3 +176,3 @@ const variableUtil = require('./variable'); | ||
// find component this propTypes/defaultProps belongs to | ||
const component = components.get(utils.getParentES6Component()); | ||
const component = components.get(componentUtil.getParentES6Component(context)); | ||
if (!component) { | ||
@@ -219,3 +220,3 @@ return; | ||
// find component this propTypes/defaultProps belongs to | ||
const component = components.get(utils.getParentES6Component()); | ||
const component = components.get(componentUtil.getParentES6Component(context)); | ||
if (!component) { | ||
@@ -246,3 +247,3 @@ return; | ||
// find component this propTypes/defaultProps belongs to | ||
const component = utils.isES5Component(node) && components.get(node); | ||
const component = componentUtil.isES5Component(node, context) && components.get(node); | ||
if (!component) { | ||
@@ -249,0 +250,0 @@ return; |
'use strict'; | ||
function docsUrl(ruleName) { | ||
return `https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules/${ruleName}.md`; | ||
return `https://github.com/jsx-eslint/eslint-plugin-react/tree/master/docs/rules/${ruleName}.md`; | ||
} | ||
module.exports = docsUrl; |
@@ -10,3 +10,3 @@ 'use strict'; | ||
const attributes = node.attributes; | ||
if (attributes.length === 0) { | ||
if (!attributes || attributes.length === 0) { | ||
return node.name; | ||
@@ -13,0 +13,0 @@ } |
@@ -7,6 +7,6 @@ /** | ||
const estraverse = require('estraverse'); | ||
const elementType = require('jsx-ast-utils/elementType'); | ||
const astUtil = require('./ast'); | ||
const isCreateElement = require('./isCreateElement'); | ||
const variableUtil = require('./variable'); | ||
@@ -90,4 +90,2 @@ | ||
* | ||
* @param {Function} isCreateElement Function to determine if a CallExpresion is | ||
* a createElement one | ||
* @param {ASTNode} ASTnode The AST node being checked | ||
@@ -99,60 +97,45 @@ * @param {Context} context The context of `ASTNode`. | ||
*/ | ||
function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) { | ||
let found = false; | ||
astUtil.traverseReturns(ASTnode, context, (node) => { | ||
// Traverse return statement | ||
astUtil.traverse(node, { | ||
enter(childNode) { | ||
const setFound = () => { | ||
found = true; | ||
this.skip(); | ||
}; | ||
switch (childNode.type) { | ||
case 'FunctionExpression': | ||
case 'FunctionDeclaration': | ||
case 'ArrowFunctionExpression': | ||
// Do not traverse into inner function definitions | ||
return this.skip(); | ||
case 'ConditionalExpression': | ||
if (!strict) break; | ||
if (isJSX(childNode.consequent) && isJSX(childNode.alternate)) { | ||
setFound(); | ||
} | ||
this.skip(); | ||
break; | ||
case 'LogicalExpression': | ||
if (!strict) break; | ||
if (isJSX(childNode.left) && isJSX(childNode.right)) { | ||
setFound(); | ||
} | ||
this.skip(); | ||
break; | ||
case 'JSXElement': | ||
case 'JSXFragment': | ||
setFound(); | ||
break; | ||
case 'CallExpression': | ||
if (isCreateElement(childNode)) { | ||
setFound(); | ||
} | ||
this.skip(); | ||
break; | ||
case 'Literal': | ||
if (!ignoreNull && childNode.value === null) { | ||
setFound(); | ||
} | ||
break; | ||
case 'Identifier': { | ||
const variable = variableUtil.findVariableByName(context, childNode.name); | ||
if (isJSX(variable)) { | ||
setFound(); | ||
} | ||
break; | ||
} | ||
default: | ||
function isReturningJSX(ASTnode, context, strict, ignoreNull) { | ||
const isJSXValue = (node) => { | ||
if (!node) { | ||
return false; | ||
} | ||
switch (node.type) { | ||
case 'ConditionalExpression': | ||
if (strict) { | ||
return isJSXValue(node.consequent) && isJSXValue(node.alternate); | ||
} | ||
}, | ||
}); | ||
return isJSXValue(node.consequent) || isJSXValue(node.alternate); | ||
case 'LogicalExpression': | ||
if (strict) { | ||
return isJSXValue(node.left) && isJSXValue(node.right); | ||
} | ||
return isJSXValue(node.left) || isJSXValue(node.right); | ||
case 'SequenceExpression': | ||
return isJSXValue(node.expressions[node.expressions.length - 1]); | ||
case 'JSXElement': | ||
case 'JSXFragment': | ||
return true; | ||
case 'CallExpression': | ||
return isCreateElement(node, context); | ||
case 'Literal': | ||
if (!ignoreNull && node.value === null) { | ||
return true; | ||
} | ||
return false; | ||
case 'Identifier': { | ||
const variable = variableUtil.findVariableByName(context, node.name); | ||
return isJSX(variable); | ||
} | ||
default: | ||
return false; | ||
} | ||
}; | ||
return found && estraverse.VisitorOption.Break; | ||
let found = false; | ||
astUtil.traverseReturns(ASTnode, context, (node, breakTraverse) => { | ||
if (isJSXValue(node)) { | ||
found = true; | ||
breakTraverse(); | ||
} | ||
}); | ||
@@ -166,4 +149,2 @@ | ||
* | ||
* @param {Function} isCreateElement Function to determine if a CallExpresion is | ||
* a createElement one | ||
* @param {ASTNode} ASTnode The AST node being checked | ||
@@ -173,3 +154,3 @@ * @param {Context} context The context of `ASTNode`. | ||
*/ | ||
function isReturningOnlyNull(isCreateElement, ASTnode, context) { | ||
function isReturningOnlyNull(ASTnode, context) { | ||
let found = false; | ||
@@ -176,0 +157,0 @@ let foundSomethingElse = false; |
@@ -76,2 +76,6 @@ /** | ||
if (shouldBeNoop(context, methodName)) { | ||
return {}; | ||
} | ||
// -------------------------------------------------------------------------- | ||
@@ -83,6 +87,2 @@ // Public | ||
CallExpression(node) { | ||
if (shouldBeNoop(context, methodName)) { | ||
return; | ||
} | ||
const callee = node.callee; | ||
@@ -100,3 +100,3 @@ if ( | ||
if (/Function(Expression|Declaration)$/.test(ancestor.type)) { | ||
depth++; | ||
depth += 1; | ||
} | ||
@@ -103,0 +103,0 @@ if ( |
@@ -12,2 +12,6 @@ /** | ||
/** | ||
* @param {Context} context | ||
* @returns {string} | ||
*/ | ||
function getCreateClassFromContext(context) { | ||
@@ -25,2 +29,6 @@ let pragma = 'createReactClass'; | ||
/** | ||
* @param {Context} context | ||
* @returns {string} | ||
*/ | ||
function getFragmentFromContext(context) { | ||
@@ -38,2 +46,6 @@ let pragma = 'Fragment'; | ||
/** | ||
* @param {Context} context | ||
* @returns {string} | ||
*/ | ||
function getFromContext(context) { | ||
@@ -40,0 +52,0 @@ let pragma = 'React'; |
@@ -108,2 +108,3 @@ /** | ||
VoidFunctionComponent: 0, | ||
VFC: 0, | ||
PropsWithChildren: 0, | ||
@@ -1019,3 +1020,3 @@ SFC: 0, | ||
declaredPropTypes: obj.declaredPropTypes, | ||
ignorePropsValidation: false, | ||
ignorePropsValidation: obj.shouldIgnorePropTypes, | ||
}); | ||
@@ -1033,3 +1034,3 @@ return; | ||
// https://github.com/yannickcr/eslint-plugin-react/issues/2784 | ||
// https://github.com/jsx-eslint/eslint-plugin-react/issues/2784 | ||
if (isInsideClassBody(node) && !astUtil.isFunction(node)) { | ||
@@ -1058,3 +1059,3 @@ return; | ||
} else { | ||
// implements what's discussed here: https://github.com/yannickcr/eslint-plugin-react/issues/2777#issuecomment-683944481 | ||
// implements what's discussed here: https://github.com/jsx-eslint/eslint-plugin-react/issues/2777#issuecomment-683944481 | ||
const annotation = siblingIdentifier.typeAnnotation.typeAnnotation; | ||
@@ -1061,0 +1062,0 @@ |
@@ -8,2 +8,3 @@ /** | ||
const astUtil = require('./ast'); | ||
const componentUtil = require('./componentUtil'); | ||
const testReactVersion = require('./version').testReactVersion; | ||
@@ -185,4 +186,8 @@ const ast = require('./ast'); | ||
function isInClassComponent(utils) { | ||
return utils.getParentES6Component() || utils.getParentES5Component(); | ||
/** | ||
* @param {Context} context | ||
* @returns {boolean} | ||
*/ | ||
function isInClassComponent(context) { | ||
return !!(componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context)); | ||
} | ||
@@ -224,3 +229,3 @@ | ||
if (isInClassComponent(utils)) { | ||
if (isInClassComponent(context)) { | ||
// this.props.* | ||
@@ -233,3 +238,3 @@ if (isThisDotProps(unwrappedObjectNode)) { | ||
isCommonVariableNameForProps(unwrappedObjectNode.name) | ||
&& (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor()) | ||
&& (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || astUtil.inConstructor(context)) | ||
) { | ||
@@ -472,3 +477,3 @@ return true; | ||
// let props = this.props | ||
if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils) && node.id.type === 'Identifier') { | ||
if (isThisDotProps(unwrappedInitNode) && isInClassComponent(context) && node.id.type === 'Identifier') { | ||
propVariables.set(node.id.name, []); | ||
@@ -509,3 +514,3 @@ } | ||
// let {firstname} = this.props | ||
if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils)) { | ||
if (isThisDotProps(unwrappedInitNode) && isInClassComponent(context)) { | ||
markPropTypesAsUsed(node.id); | ||
@@ -512,0 +517,0 @@ return; |
@@ -90,3 +90,3 @@ /** | ||
error('Warning: React version not specified in eslint-plugin-react settings. ' | ||
+ 'See https://github.com/yannickcr/eslint-plugin-react#configuration .'); | ||
+ 'See https://github.com/jsx-eslint/eslint-plugin-react#configuration .'); | ||
warnedForMissingVersion = true; | ||
@@ -93,0 +93,0 @@ } |
{ | ||
"name": "eslint-plugin-react", | ||
"version": "7.29.4", | ||
"version": "7.30.0", | ||
"author": "Yannick Croissant <yannick.croissant+npm@gmail.com>", | ||
@@ -8,2 +8,3 @@ "description": "React specific linting rules for ESLint", | ||
"scripts": { | ||
"prepack": "npmignore --auto --commentLines=autogenerated", | ||
"lint": "eslint .", | ||
@@ -19,17 +20,11 @@ "postlint": "npm run type-check", | ||
}, | ||
"files": [ | ||
"LICENSE", | ||
"README.md", | ||
"index.js", | ||
"lib" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/yannickcr/eslint-plugin-react" | ||
"url": "https://github.com/jsx-eslint/eslint-plugin-react" | ||
}, | ||
"homepage": "https://github.com/yannickcr/eslint-plugin-react", | ||
"bugs": "https://github.com/yannickcr/eslint-plugin-react/issues", | ||
"homepage": "https://github.com/jsx-eslint/eslint-plugin-react", | ||
"bugs": "https://github.com/jsx-eslint/eslint-plugin-react/issues", | ||
"dependencies": { | ||
"array-includes": "^3.1.4", | ||
"array.prototype.flatmap": "^1.2.5", | ||
"array-includes": "^3.1.5", | ||
"array.prototype.flatmap": "^1.3.0", | ||
"doctrine": "^2.1.0", | ||
@@ -41,3 +36,3 @@ "estraverse": "^5.3.0", | ||
"object.fromentries": "^2.0.5", | ||
"object.hasown": "^1.1.0", | ||
"object.hasown": "^1.1.1", | ||
"object.values": "^1.1.5", | ||
@@ -47,14 +42,14 @@ "prop-types": "^15.8.1", | ||
"semver": "^6.3.0", | ||
"string.prototype.matchall": "^4.0.6" | ||
"string.prototype.matchall": "^4.0.7" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.17.5", | ||
"@babel/core": "^7.17.12", | ||
"@babel/eslint-parser": "^7.17.0", | ||
"@babel/plugin-syntax-decorators": "^7.17.0", | ||
"@babel/plugin-syntax-decorators": "^7.17.12", | ||
"@babel/plugin-syntax-do-expressions": "^7.16.7", | ||
"@babel/plugin-syntax-function-bind": "^7.16.7", | ||
"@babel/preset-react": "^7.16.7", | ||
"@babel/preset-react": "^7.17.12", | ||
"@types/eslint": "=7.2.10", | ||
"@types/estree": "^0.0.50", | ||
"@types/node": "^16.11.26", | ||
"@types/estree": "0.0.51", | ||
"@types/node": "^16.11.35", | ||
"@typescript-eslint/parser": "^2.34.0 || ^3.10.1 || ^4.0.0 || ^5.0.0", | ||
@@ -66,11 +61,12 @@ "aud": "^2.0.0", | ||
"eslint-plugin-eslint-plugin": "^2.3.0 || ^3.5.3 || ^4.0.1", | ||
"eslint-plugin-import": "^2.25.4", | ||
"eslint-remote-tester": "^2.1.1", | ||
"eslint-remote-tester-repositories": "^0.0.4", | ||
"eslint-plugin-import": "^2.26.0", | ||
"eslint-remote-tester": "^2.1.4", | ||
"eslint-remote-tester-repositories": "^0.0.5", | ||
"eslint-scope": "^3.7.3", | ||
"espree": "^3.5.4", | ||
"istanbul": "^0.4.5", | ||
"ls-engines": "^0.6.5", | ||
"ls-engines": "^0.6.6", | ||
"markdown-magic": "^2.6.0", | ||
"mocha": "^5.2.0", | ||
"npmignore": "^0.3.0", | ||
"sinon": "^7.5.0", | ||
@@ -93,7 +89,16 @@ "typescript": "^3.9.9", | ||
"license": "MIT", | ||
"greenkeeper": { | ||
"publishConfig": { | ||
"ignore": [ | ||
"semver" | ||
".github/", | ||
"!lib", | ||
"docs/", | ||
"test/", | ||
"tests/", | ||
"*.md", | ||
"*.config.js", | ||
".eslintrc", | ||
".editorconfig", | ||
"tsconfig.json" | ||
] | ||
} | ||
} |
`eslint-plugin-react` | ||
=================== | ||
[![Maintenance Status][status-image]][status-url] [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][deps-image]][deps-url] [![Code Climate][climate-image]][climate-url] [![Tidelift][tidelift-image]][tidelift-url] | ||
[![Maintenance Status][status-image]][status-url] [![NPM version][npm-image]][npm-url] [![Dependency Status][deps-image]][deps-url] [![Code Climate][climate-image]][climate-url] [![Tidelift][tidelift-image]][tidelift-url] | ||
@@ -10,14 +10,8 @@ React specific linting rules for `eslint` | ||
Install [`eslint`](https://www.github.com/eslint/eslint) either locally or globally. (Note that locally, per project, is strongly preferred) | ||
```sh | ||
$ npm install eslint@7 --save-dev | ||
$ npm install eslint eslint-plugin-react --save-dev | ||
``` | ||
If you installed `eslint` globally, you have to install the React plugin globally too. Otherwise, install it locally (strongly preferred) | ||
It is also possible to install ESLint globally rather than locally (using npm install eslint --global). However, this is not recommended, and any plugins or shareable configs that you use must be installed locally in either case. | ||
```sh | ||
$ npm install eslint-plugin-react --save-dev | ||
``` | ||
# Configuration | ||
@@ -35,3 +29,3 @@ | ||
If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), extend [`react/jsx-runtime`](https://github.com/yannickcr/eslint-plugin-react/blob/c8917b0885094b5e4cc2a6f613f7fb6f16fe932e/index.js#L163-L176) in your eslint config (add `"plugin:react/jsx-runtime"` to `"extends"`) to disable the relevant rules. | ||
If you are using the [new JSX transform from React 17](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports), extend [`react/jsx-runtime`](https://github.com/jsx-eslint/eslint-plugin-react/blob/c8917b0885094b5e4cc2a6f613f7fb6f16fe932e/index.js#L163-L176) in your eslint config (add `"plugin:react/jsx-runtime"` to `"extends"`) to disable the relevant rules. | ||
@@ -128,3 +122,3 @@ You should also specify settings that will be shared across all the plugin rules. ([More about eslint shared settings](https://eslint.org/docs/user-guide/configuring/configuration-files#adding-shared-settings)) | ||
| | | [react/default-props-match-prop-types](docs/rules/default-props-match-prop-types.md) | Enforce all defaultProps are defined and not "required" in propTypes. | | ||
| | | [react/destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context | | ||
| | 🔧 | [react/destructuring-assignment](docs/rules/destructuring-assignment.md) | Enforce consistent usage of destructuring assignment of props, state, and context | | ||
| ✔ | | [react/display-name](docs/rules/display-name.md) | Prevent missing displayName in a React component definition | | ||
@@ -214,2 +208,3 @@ | | | [react/forbid-component-props](docs/rules/forbid-component-props.md) | Forbid certain props on components | | ||
| ✔ | | [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md) | Enforce no duplicate props | | ||
| | 🔧 | [react/jsx-no-leaked-render](docs/rules/jsx-no-leaked-render.md) | Prevent problematic leaked values from being rendered | | ||
| | | [react/jsx-no-literals](docs/rules/jsx-no-literals.md) | Prevent using string literals in React component definition | | ||
@@ -279,15 +274,12 @@ | | | [react/jsx-no-script-url](docs/rules/jsx-no-script-url.md) | Forbid `javascript:` URLs | | ||
[travis-url]: https://travis-ci.org/yannickcr/eslint-plugin-react | ||
[travis-image]: https://img.shields.io/travis/yannickcr/eslint-plugin-react/master.svg | ||
[deps-url]: https://david-dm.org/jsx-eslint/eslint-plugin-react | ||
[deps-image]: https://img.shields.io/david/dev/jsx-eslint/eslint-plugin-react.svg | ||
[deps-url]: https://david-dm.org/yannickcr/eslint-plugin-react | ||
[deps-image]: https://img.shields.io/david/dev/yannickcr/eslint-plugin-react.svg | ||
[climate-url]: https://codeclimate.com/github/jsx-eslint/eslint-plugin-react | ||
[climate-image]: https://img.shields.io/codeclimate/maintainability/jsx-eslint/eslint-plugin-react.svg | ||
[climate-url]: https://codeclimate.com/github/yannickcr/eslint-plugin-react | ||
[climate-image]: https://img.shields.io/codeclimate/maintainability/yannickcr/eslint-plugin-react.svg | ||
[status-url]: https://github.com/jsx-eslint/eslint-plugin-react/pulse | ||
[status-image]: https://img.shields.io/github/last-commit/jsx-eslint/eslint-plugin-react.svg | ||
[status-url]: https://github.com/yannickcr/eslint-plugin-react/pulse | ||
[status-image]: https://img.shields.io/github/last-commit/yannickcr/eslint-plugin-react.svg | ||
[tidelift-url]: https://tidelift.com/subscription/pkg/npm-eslint-plugin-react?utm_source=npm-eslint-plugin-react&utm_medium=referral&utm_campaign=readme | ||
[tidelift-image]: https://tidelift.com/badges/github/yannickcr/eslint-plugin-react?style=flat | ||
[tidelift-image]: https://tidelift.com/badges/github/jsx-eslint/eslint-plugin-react?style=flat |
719615
128
20078
28
281
Updatedarray-includes@^3.1.5
Updatedobject.hasown@^1.1.1