From 10c6f23866b58dff5a7f8d16eb8b8a62cc997a66 Mon Sep 17 00:00:00 2001 From: David Petersen Date: Wed, 27 Jul 2016 17:29:17 -0500 Subject: [PATCH] Switch to new eslint rule format (fixes #661) --- lib/rules/display-name.js | 390 ++--- lib/rules/forbid-prop-types.js | 210 +-- lib/rules/jsx-boolean-value.js | 81 +- lib/rules/jsx-closing-bracket-location.js | 437 +++--- lib/rules/jsx-curly-spacing.js | 409 +++--- lib/rules/jsx-equals-spacing.js | 161 ++- lib/rules/jsx-filename-extension.js | 120 +- lib/rules/jsx-first-prop-new-line.js | 62 +- lib/rules/jsx-handler-names.js | 96 +- lib/rules/jsx-indent-props.js | 249 ++-- lib/rules/jsx-indent.js | 256 ++-- lib/rules/jsx-key.js | 99 +- lib/rules/jsx-max-props-per-line.js | 92 +- lib/rules/jsx-no-bind.js | 160 ++- lib/rules/jsx-no-comment-textnodes.js | 52 +- lib/rules/jsx-no-duplicate-props.js | 84 +- lib/rules/jsx-no-literals.js | 68 +- lib/rules/jsx-no-target-blank.js | 45 +- lib/rules/jsx-no-undef.js | 123 +- lib/rules/jsx-pascal-case.js | 80 +- lib/rules/jsx-require-extension.js | 112 +- lib/rules/jsx-sort-props.js | 178 +-- lib/rules/jsx-space-before-closing.js | 111 +- lib/rules/jsx-uses-react.js | 35 +- lib/rules/jsx-uses-vars.js | 63 +- lib/rules/jsx-wrap-multilines.js | 137 +- lib/rules/no-comment-textnodes.js | 44 +- lib/rules/no-danger.js | 35 +- lib/rules/no-deprecated.js | 151 +- lib/rules/no-did-mount-set-state.js | 78 +- lib/rules/no-did-update-set-state.js | 78 +- lib/rules/no-direct-mutation-state.js | 118 +- lib/rules/no-find-dom-node.js | 49 +- lib/rules/no-is-mounted.js | 55 +- lib/rules/no-multi-comp.js | 92 +- lib/rules/no-render-return-value.js | 85 +- lib/rules/no-set-state.js | 55 +- lib/rules/no-string-refs.js | 157 ++- lib/rules/no-unknown-property.js | 83 +- lib/rules/prefer-es6-class.js | 54 +- lib/rules/prefer-stateless-function.js | 553 ++++---- lib/rules/prop-types.js | 1561 +++++++++++---------- lib/rules/react-in-jsx-scope.js | 51 +- lib/rules/require-extension.js | 56 +- lib/rules/require-optimization.js | 340 ++--- lib/rules/require-render-return.js | 189 +-- lib/rules/self-closing-comp.js | 110 +- lib/rules/sort-comp.js | 638 ++++----- lib/rules/sort-prop-types.js | 278 ++-- lib/rules/wrap-multilines.js | 64 +- 50 files changed, 4588 insertions(+), 4296 deletions(-) diff --git a/lib/rules/display-name.js b/lib/rules/display-name.js index f2abb00f24..2979584d69 100644 --- a/lib/rules/display-name.js +++ b/lib/rules/display-name.js @@ -10,214 +10,220 @@ var Components = require('../util/Components'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components, utils) { - - var sourceCode = context.getSourceCode(); - var config = context.options[0] || {}; - var ignoreTranspilerName = config.ignoreTranspilerName || false; - - var MISSING_MESSAGE = 'Component definition is missing display name'; - - /** - * Checks if we are declaring a display name - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are declaring a display name, false if not. - */ - function isDisplayNameDeclaration(node) { - switch (node.type) { - // Special case for class properties - // (babel-eslint does not expose property name so we have to rely on tokens) - case 'ClassProperty': - var tokens = sourceCode.getFirstTokens(node, 2); - if ( - tokens[0].value === 'displayName' || - (tokens[1] && tokens[1].value === 'displayName') - ) { - return true; +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: { + ignoreTranspilerName: { + type: 'boolean' } - return false; - case 'Identifier': - return node.name === 'displayName'; - case 'Literal': - return node.value === 'displayName'; - default: - return false; - } - } - - /** - * Mark a prop type as declared - * @param {ASTNode} node The AST node being checked. - */ - function markDisplayNameAsDeclared(node) { - components.set(node, { - hasDisplayName: true - }); - } - - /** - * Reports missing display name for a given component - * @param {Object} component The component to process - */ - function reportMissingDisplayName(component) { - context.report({ - node: component.node, - message: MISSING_MESSAGE, - data: { - component: component.name + }, + additionalProperties: false + }] + }, + + create: Components.detect(function(context, components, utils) { + + var sourceCode = context.getSourceCode(); + var config = context.options[0] || {}; + var ignoreTranspilerName = config.ignoreTranspilerName || false; + + var MISSING_MESSAGE = 'Component definition is missing display name'; + + /** + * Checks if we are declaring a display name + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if we are declaring a display name, false if not. + */ + function isDisplayNameDeclaration(node) { + switch (node.type) { + // Special case for class properties + // (babel-eslint does not expose property name so we have to rely on tokens) + case 'ClassProperty': + var tokens = sourceCode.getFirstTokens(node, 2); + if ( + tokens[0].value === 'displayName' || + (tokens[1] && tokens[1].value === 'displayName') + ) { + return true; + } + return false; + case 'Identifier': + return node.name === 'displayName'; + case 'Literal': + return node.value === 'displayName'; + default: + return false; } - }); - } - - /** - * Checks if the component have a name set by the transpiler - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True ifcomponent have a name, false if not. - */ - function hasTranspilerName(node) { - var namedObjectAssignment = ( - node.type === 'ObjectExpression' && - node.parent && - node.parent.parent && - node.parent.parent.type === 'AssignmentExpression' && ( - !node.parent.parent.left.object || - node.parent.parent.left.object.name !== 'module' || - node.parent.parent.left.property.name !== 'exports' - ) - ); - var namedObjectDeclaration = ( + } + + /** + * Mark a prop type as declared + * @param {ASTNode} node The AST node being checked. + */ + function markDisplayNameAsDeclared(node) { + components.set(node, { + hasDisplayName: true + }); + } + + /** + * Reports missing display name for a given component + * @param {Object} component The component to process + */ + function reportMissingDisplayName(component) { + context.report({ + node: component.node, + message: MISSING_MESSAGE, + data: { + component: component.name + } + }); + } + + /** + * Checks if the component have a name set by the transpiler + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True ifcomponent have a name, false if not. + */ + function hasTranspilerName(node) { + var namedObjectAssignment = ( node.type === 'ObjectExpression' && node.parent && node.parent.parent && - node.parent.parent.type === 'VariableDeclarator' - ); - var namedClass = ( - (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') && - node.id && - node.id.name - ); - - var namedFunctionDeclaration = ( - (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') && - node.id && - node.id.name - ); - - var namedFunctionExpression = ( - (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && - node.parent && - (node.parent.type === 'VariableDeclarator' || node.parent.method === true) - ); - - if ( - namedObjectAssignment || namedObjectDeclaration || - namedClass || - namedFunctionDeclaration || namedFunctionExpression - ) { - return true; + node.parent.parent.type === 'AssignmentExpression' && ( + !node.parent.parent.left.object || + node.parent.parent.left.object.name !== 'module' || + node.parent.parent.left.property.name !== 'exports' + ) + ); + var namedObjectDeclaration = ( + node.type === 'ObjectExpression' && + node.parent && + node.parent.parent && + node.parent.parent.type === 'VariableDeclarator' + ); + var namedClass = ( + (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') && + node.id && + node.id.name + ); + + var namedFunctionDeclaration = ( + (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') && + node.id && + node.id.name + ); + + var namedFunctionExpression = ( + (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && + node.parent && + (node.parent.type === 'VariableDeclarator' || node.parent.method === true) + ); + + if ( + namedObjectAssignment || namedObjectDeclaration || + namedClass || + namedFunctionDeclaration || namedFunctionExpression + ) { + return true; + } + return false; } - return false; - } - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - return { + return { - ClassProperty: function(node) { - if (!isDisplayNameDeclaration(node)) { - return; - } - markDisplayNameAsDeclared(node); - }, + ClassProperty: function(node) { + if (!isDisplayNameDeclaration(node)) { + return; + } + markDisplayNameAsDeclared(node); + }, - MemberExpression: function(node) { - if (!isDisplayNameDeclaration(node.property)) { - return; - } - var component = utils.getRelatedComponent(node); - if (!component) { - return; - } - markDisplayNameAsDeclared(component.node); - }, + MemberExpression: function(node) { + if (!isDisplayNameDeclaration(node.property)) { + return; + } + var component = utils.getRelatedComponent(node); + if (!component) { + return; + } + markDisplayNameAsDeclared(component.node); + }, - FunctionExpression: function(node) { - if (ignoreTranspilerName || !hasTranspilerName(node)) { - return; - } - markDisplayNameAsDeclared(node); - }, + FunctionExpression: function(node) { + if (ignoreTranspilerName || !hasTranspilerName(node)) { + return; + } + markDisplayNameAsDeclared(node); + }, - FunctionDeclaration: function(node) { - if (ignoreTranspilerName || !hasTranspilerName(node)) { - return; - } - markDisplayNameAsDeclared(node); - }, + FunctionDeclaration: function(node) { + if (ignoreTranspilerName || !hasTranspilerName(node)) { + return; + } + markDisplayNameAsDeclared(node); + }, - ArrowFunctionExpression: function(node) { - if (ignoreTranspilerName || !hasTranspilerName(node)) { - return; - } - markDisplayNameAsDeclared(node); - }, + ArrowFunctionExpression: function(node) { + if (ignoreTranspilerName || !hasTranspilerName(node)) { + return; + } + markDisplayNameAsDeclared(node); + }, - MethodDefinition: function(node) { - if (!isDisplayNameDeclaration(node.key)) { - return; - } - markDisplayNameAsDeclared(node); - }, + MethodDefinition: function(node) { + if (!isDisplayNameDeclaration(node.key)) { + return; + } + markDisplayNameAsDeclared(node); + }, - ClassExpression: function(node) { - if (ignoreTranspilerName || !hasTranspilerName(node)) { - return; - } - markDisplayNameAsDeclared(node); - }, + ClassExpression: function(node) { + if (ignoreTranspilerName || !hasTranspilerName(node)) { + return; + } + markDisplayNameAsDeclared(node); + }, - ClassDeclaration: function(node) { - if (ignoreTranspilerName || !hasTranspilerName(node)) { - return; - } - markDisplayNameAsDeclared(node); - }, - - ObjectExpression: function(node) { - if (ignoreTranspilerName || !hasTranspilerName(node)) { - // Search for the displayName declaration - node.properties.forEach(function(property) { - if (!property.key || !isDisplayNameDeclaration(property.key)) { - return; + ClassDeclaration: function(node) { + if (ignoreTranspilerName || !hasTranspilerName(node)) { + return; + } + markDisplayNameAsDeclared(node); + }, + + ObjectExpression: function(node) { + if (ignoreTranspilerName || !hasTranspilerName(node)) { + // Search for the displayName declaration + node.properties.forEach(function(property) { + if (!property.key || !isDisplayNameDeclaration(property.key)) { + return; + } + markDisplayNameAsDeclared(node); + }); + return; + } + markDisplayNameAsDeclared(node); + }, + + 'Program:exit': function() { + var list = components.list(); + // Report missing display name for all components + for (var component in list) { + if (!list.hasOwnProperty(component) || list[component].hasDisplayName) { + continue; } - markDisplayNameAsDeclared(node); - }); - return; - } - markDisplayNameAsDeclared(node); - }, - - 'Program:exit': function() { - var list = components.list(); - // Report missing display name for all components - for (var component in list) { - if (!list.hasOwnProperty(component) || list[component].hasDisplayName) { - continue; + reportMissingDisplayName(list[component]); } - reportMissingDisplayName(list[component]); } - } - }; -}); - -module.exports.schema = [{ - type: 'object', - properties: { - ignoreTranspilerName: { - type: 'boolean' - } - }, - additionalProperties: false -}]; + }; + }) +}; diff --git a/lib/rules/forbid-prop-types.js b/lib/rules/forbid-prop-types.js index 19b10452f6..99bd667224 100644 --- a/lib/rules/forbid-prop-types.js +++ b/lib/rules/forbid-prop-types.js @@ -13,122 +13,128 @@ var DEFAULTS = ['any', 'array', 'object']; // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: { + forbid: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: true + }] + }, - function isForbidden(type) { - var configuration = context.options[0] || {}; + create: function(context) { - var forbid = configuration.forbid || DEFAULTS; - return forbid.indexOf(type) >= 0; - } + function isForbidden(type) { + var configuration = context.options[0] || {}; - /** - * Checks if node is `propTypes` declaration - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if node is `propTypes` declaration, false if not. - */ - function isPropTypesDeclaration(node) { - - // Special case for class properties - // (babel-eslint does not expose property name so we have to rely on tokens) - if (node.type === 'ClassProperty') { - var tokens = context.getFirstTokens(node, 2); - if (tokens[0].value === 'propTypes' || (tokens[1] && tokens[1].value === 'propTypes')) { - return true; - } - return false; + var forbid = configuration.forbid || DEFAULTS; + return forbid.indexOf(type) >= 0; } - return Boolean( - node && - node.name === 'propTypes' - ); - } - - - /** - * Checks if propTypes declarations are forbidden - * @param {Array} declarations The array of AST nodes being checked. - * @returns {void} - */ - function checkForbidden(declarations) { - declarations.forEach(function(declaration) { - if (declaration.type !== 'Property') { - return; - } - var target; - var value = declaration.value; - if ( - value.type === 'MemberExpression' && - value.property && - value.property.name && - value.property.name === 'isRequired' - ) { - value = value.object; - } - if ( - value.type === 'CallExpression' && - value.callee.type === 'MemberExpression' - ) { - value = value.callee; - } - if (value.property) { - target = value.property.name; - } else if (value.type === 'Identifier') { - target = value.name; - } - if (isForbidden(target)) { - context.report({ - node: declaration, - message: 'Prop type `' + target + '` is forbidden' - }); + /** + * Checks if node is `propTypes` declaration + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if node is `propTypes` declaration, false if not. + */ + function isPropTypesDeclaration(node) { + + // Special case for class properties + // (babel-eslint does not expose property name so we have to rely on tokens) + if (node.type === 'ClassProperty') { + var tokens = context.getFirstTokens(node, 2); + if (tokens[0].value === 'propTypes' || (tokens[1] && tokens[1].value === 'propTypes')) { + return true; + } + return false; } - }); - } - return { - ClassProperty: function(node) { - if (isPropTypesDeclaration(node) && node.value && node.value.type === 'ObjectExpression') { - checkForbidden(node.value.properties); - } - }, + return Boolean( + node && + node.name === 'propTypes' + ); + } - MemberExpression: function(node) { - if (isPropTypesDeclaration(node.property)) { - var right = node.parent.right; - if (right && right.type === 'ObjectExpression') { - checkForbidden(right.properties); - } - } - }, - ObjectExpression: function(node) { - node.properties.forEach(function(property) { - if (!property.key) { + /** + * Checks if propTypes declarations are forbidden + * @param {Array} declarations The array of AST nodes being checked. + * @returns {void} + */ + function checkForbidden(declarations) { + declarations.forEach(function(declaration) { + if (declaration.type !== 'Property') { return; } - - if (!isPropTypesDeclaration(property.key)) { - return; + var target; + var value = declaration.value; + if ( + value.type === 'MemberExpression' && + value.property && + value.property.name && + value.property.name === 'isRequired' + ) { + value = value.object; } - if (property.value.type === 'ObjectExpression') { - checkForbidden(property.value.properties); + if ( + value.type === 'CallExpression' && + value.callee.type === 'MemberExpression' + ) { + value = value.callee; + } + if (value.property) { + target = value.property.name; + } else if (value.type === 'Identifier') { + target = value.name; + } + if (isForbidden(target)) { + context.report({ + node: declaration, + message: 'Prop type `' + target + '` is forbidden' + }); } }); } - }; -}; - -module.exports.schema = [{ - type: 'object', - properties: { - forbid: { - type: 'array', - items: { - type: 'string' + return { + ClassProperty: function(node) { + if (isPropTypesDeclaration(node) && node.value && node.value.type === 'ObjectExpression') { + checkForbidden(node.value.properties); + } + }, + + MemberExpression: function(node) { + if (isPropTypesDeclaration(node.property)) { + var right = node.parent.right; + if (right && right.type === 'ObjectExpression') { + checkForbidden(right.properties); + } + } + }, + + ObjectExpression: function(node) { + node.properties.forEach(function(property) { + if (!property.key) { + return; + } + + if (!isPropTypesDeclaration(property.key)) { + return; + } + if (property.value.type === 'ObjectExpression') { + checkForbidden(property.value.properties); + } + }); } - } - }, - additionalProperties: true -}]; + + }; + } +}; diff --git a/lib/rules/jsx-boolean-value.js b/lib/rules/jsx-boolean-value.js index e4169e01d0..64452ddeac 100644 --- a/lib/rules/jsx-boolean-value.js +++ b/lib/rules/jsx-boolean-value.js @@ -8,45 +8,52 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + fixable: 'code', - var configuration = context.options[0] || 'never'; + schema: [{ + enum: ['always', 'never'] + }] + }, - var NEVER_MESSAGE = 'Value must be omitted for boolean attributes'; - var ALWAYS_MESSAGE = 'Value must be set for boolean attributes'; + create: function(context) { - return { - JSXAttribute: function(node) { - switch (configuration) { - case 'always': - if (node.value === null) { - context.report({ - node: node, - message: ALWAYS_MESSAGE, - fix: function(fixer) { - return fixer.insertTextAfter(node, '={true}'); - } - }); - } - break; - case 'never': - if (node.value && node.value.type === 'JSXExpressionContainer' && node.value.expression.value === true) { - context.report({ - node: node, - message: NEVER_MESSAGE, - fix: function(fixer) { - return fixer.removeRange([node.name.range[1], node.value.range[1]]); - } - }); - } - break; - default: - break; + var configuration = context.options[0] || 'never'; + + var NEVER_MESSAGE = 'Value must be omitted for boolean attributes'; + var ALWAYS_MESSAGE = 'Value must be set for boolean attributes'; + + return { + JSXAttribute: function(node) { + switch (configuration) { + case 'always': + if (node.value === null) { + context.report({ + node: node, + message: ALWAYS_MESSAGE, + fix: function(fixer) { + return fixer.insertTextAfter(node, '={true}'); + } + }); + } + break; + case 'never': + if (node.value && node.value.type === 'JSXExpressionContainer' && node.value.expression.value === true) { + context.report({ + node: node, + message: NEVER_MESSAGE, + fix: function(fixer) { + return fixer.removeRange([node.name.range[1], node.value.range[1]]); + } + }); + } + break; + default: + break; + } } - } - }; + }; + } }; - -module.exports.schema = [{ - enum: ['always', 'never'] -}]; diff --git a/lib/rules/jsx-closing-bracket-location.js b/lib/rules/jsx-closing-bracket-location.js index 47bd4bd260..62a7b6bb04 100644 --- a/lib/rules/jsx-closing-bracket-location.js +++ b/lib/rules/jsx-closing-bracket-location.js @@ -7,242 +7,249 @@ // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - - var MESSAGE = 'The closing bracket must be {{location}}{{details}}'; - var MESSAGE_LOCATION = { - 'after-props': 'placed after the last prop', - 'after-tag': 'placed after the opening tag', - 'props-aligned': 'aligned with the last prop', - 'tag-aligned': 'aligned with the opening tag', - 'line-aligned': 'aligned with the line containing the opening tag' - }; - var DEFAULT_LOCATION = 'tag-aligned'; - - var sourceCode = context.getSourceCode(); - var config = context.options[0]; - var options = { - nonEmpty: DEFAULT_LOCATION, - selfClosing: DEFAULT_LOCATION - }; - - if (typeof config === 'string') { - // simple shorthand [1, 'something'] - options.nonEmpty = config; - options.selfClosing = config; - } else if (typeof config === 'object') { - // [1, {location: 'something'}] (back-compat) - if (config.hasOwnProperty('location')) { - options.nonEmpty = config.location; - options.selfClosing = config.location; - } - // [1, {nonEmpty: 'something'}] - if (config.hasOwnProperty('nonEmpty')) { - options.nonEmpty = config.nonEmpty; - } - // [1, {selfClosing: 'something'}] - if (config.hasOwnProperty('selfClosing')) { - options.selfClosing = config.selfClosing; +module.exports = { + meta: { + docs: {}, + fixable: 'code', + + schema: [{ + oneOf: [ + { + enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'] + }, + { + type: 'object', + properties: { + location: { + enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'] + } + }, + additionalProperties: false + }, { + type: 'object', + properties: { + nonEmpty: { + enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false] + }, + selfClosing: { + enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false] + } + }, + additionalProperties: false + } + ] + }] + }, + + create: function(context) { + + var MESSAGE = 'The closing bracket must be {{location}}{{details}}'; + var MESSAGE_LOCATION = { + 'after-props': 'placed after the last prop', + 'after-tag': 'placed after the opening tag', + 'props-aligned': 'aligned with the last prop', + 'tag-aligned': 'aligned with the opening tag', + 'line-aligned': 'aligned with the line containing the opening tag' + }; + var DEFAULT_LOCATION = 'tag-aligned'; + + var sourceCode = context.getSourceCode(); + var config = context.options[0]; + var options = { + nonEmpty: DEFAULT_LOCATION, + selfClosing: DEFAULT_LOCATION + }; + + if (typeof config === 'string') { + // simple shorthand [1, 'something'] + options.nonEmpty = config; + options.selfClosing = config; + } else if (typeof config === 'object') { + // [1, {location: 'something'}] (back-compat) + if (config.hasOwnProperty('location')) { + options.nonEmpty = config.location; + options.selfClosing = config.location; + } + // [1, {nonEmpty: 'something'}] + if (config.hasOwnProperty('nonEmpty')) { + options.nonEmpty = config.nonEmpty; + } + // [1, {selfClosing: 'something'}] + if (config.hasOwnProperty('selfClosing')) { + options.selfClosing = config.selfClosing; + } } - } - /** - * Get expected location for the closing bracket - * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop - * @return {String} Expected location for the closing bracket - */ - function getExpectedLocation(tokens) { - var location; - // Is always after the opening tag if there is no props - if (typeof tokens.lastProp === 'undefined') { - location = 'after-tag'; - // Is always after the last prop if this one is on the same line as the opening bracket - } else if (tokens.opening.line === tokens.lastProp.firstLine) { - location = 'after-props'; - // Else use configuration dependent on selfClosing property - } else { - location = tokens.selfClosing ? options.selfClosing : options.nonEmpty; + /** + * Get expected location for the closing bracket + * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop + * @return {String} Expected location for the closing bracket + */ + function getExpectedLocation(tokens) { + var location; + // Is always after the opening tag if there is no props + if (typeof tokens.lastProp === 'undefined') { + location = 'after-tag'; + // Is always after the last prop if this one is on the same line as the opening bracket + } else if (tokens.opening.line === tokens.lastProp.firstLine) { + location = 'after-props'; + // Else use configuration dependent on selfClosing property + } else { + location = tokens.selfClosing ? options.selfClosing : options.nonEmpty; + } + return location; } - return location; - } - /** - * Get the correct 0-indexed column for the closing bracket, given the - * expected location. - * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop - * @param {String} expectedLocation Expected location for the closing bracket - * @return {?Number} The correct column for the closing bracket, or null - */ - function getCorrectColumn(tokens, expectedLocation) { - switch (expectedLocation) { - case 'props-aligned': - return tokens.lastProp.column; - case 'tag-aligned': - return tokens.opening.column; - case 'line-aligned': - return tokens.openingStartOfLine.column; - default: - return null; + /** + * Get the correct 0-indexed column for the closing bracket, given the + * expected location. + * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop + * @param {String} expectedLocation Expected location for the closing bracket + * @return {?Number} The correct column for the closing bracket, or null + */ + function getCorrectColumn(tokens, expectedLocation) { + switch (expectedLocation) { + case 'props-aligned': + return tokens.lastProp.column; + case 'tag-aligned': + return tokens.opening.column; + case 'line-aligned': + return tokens.openingStartOfLine.column; + default: + return null; + } } - } - /** - * Check if the closing bracket is correctly located - * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop - * @param {String} expectedLocation Expected location for the closing bracket - * @return {Boolean} True if the closing bracket is correctly located, false if not - */ - function hasCorrectLocation(tokens, expectedLocation) { - switch (expectedLocation) { - case 'after-tag': - return tokens.tag.line === tokens.closing.line; - case 'after-props': - return tokens.lastProp.lastLine === tokens.closing.line; - case 'props-aligned': - case 'tag-aligned': - case 'line-aligned': - var correctColumn = getCorrectColumn(tokens, expectedLocation); - return correctColumn === tokens.closing.column; - default: - return true; + /** + * Check if the closing bracket is correctly located + * @param {Object} tokens Locations of the opening bracket, closing bracket and last prop + * @param {String} expectedLocation Expected location for the closing bracket + * @return {Boolean} True if the closing bracket is correctly located, false if not + */ + function hasCorrectLocation(tokens, expectedLocation) { + switch (expectedLocation) { + case 'after-tag': + return tokens.tag.line === tokens.closing.line; + case 'after-props': + return tokens.lastProp.lastLine === tokens.closing.line; + case 'props-aligned': + case 'tag-aligned': + case 'line-aligned': + var correctColumn = getCorrectColumn(tokens, expectedLocation); + return correctColumn === tokens.closing.column; + default: + return true; + } } - } - /** - * Get the locations of the opening bracket, closing bracket, last prop, and - * start of opening line. - * @param {ASTNode} node The node to check - * @return {Object} Locations of the opening bracket, closing bracket, last - * prop and start of opening line. - */ - function getTokensLocations(node) { - var opening = sourceCode.getFirstToken(node).loc.start; - var closing = sourceCode.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start; - var tag = sourceCode.getFirstToken(node.name).loc.start; - var lastProp; - if (node.attributes.length) { - lastProp = node.attributes[node.attributes.length - 1]; - lastProp = { - column: sourceCode.getFirstToken(lastProp).loc.start.column, - firstLine: sourceCode.getFirstToken(lastProp).loc.start.line, - lastLine: sourceCode.getLastToken(lastProp).loc.end.line + /** + * Get the locations of the opening bracket, closing bracket, last prop, and + * start of opening line. + * @param {ASTNode} node The node to check + * @return {Object} Locations of the opening bracket, closing bracket, last + * prop and start of opening line. + */ + function getTokensLocations(node) { + var opening = sourceCode.getFirstToken(node).loc.start; + var closing = sourceCode.getLastTokens(node, node.selfClosing ? 2 : 1)[0].loc.start; + var tag = sourceCode.getFirstToken(node.name).loc.start; + var lastProp; + if (node.attributes.length) { + lastProp = node.attributes[node.attributes.length - 1]; + lastProp = { + column: sourceCode.getFirstToken(lastProp).loc.start.column, + firstLine: sourceCode.getFirstToken(lastProp).loc.start.line, + lastLine: sourceCode.getLastToken(lastProp).loc.end.line + }; + } + var openingLine = sourceCode.lines[opening.line - 1]; + var openingStartOfLine = { + column: /^\s*/.exec(openingLine)[0].length, + line: opening.line + }; + return { + tag: tag, + opening: opening, + closing: closing, + lastProp: lastProp, + selfClosing: node.selfClosing, + openingStartOfLine: openingStartOfLine }; } - var openingLine = sourceCode.lines[opening.line - 1]; - var openingStartOfLine = { - column: /^\s*/.exec(openingLine)[0].length, - line: opening.line - }; - return { - tag: tag, - opening: opening, - closing: closing, - lastProp: lastProp, - selfClosing: node.selfClosing, - openingStartOfLine: openingStartOfLine - }; - } - /** - * Get an unique ID for a given JSXOpeningElement - * - * @param {ASTNode} node The AST node being checked. - * @returns {String} Unique ID (based on its range) - */ - function getOpeningElementId(node) { - return node.range.join(':'); - } + /** + * Get an unique ID for a given JSXOpeningElement + * + * @param {ASTNode} node The AST node being checked. + * @returns {String} Unique ID (based on its range) + */ + function getOpeningElementId(node) { + return node.range.join(':'); + } - var lastAttributeNode = {}; + var lastAttributeNode = {}; - return { - JSXAttribute: function(node) { - lastAttributeNode[getOpeningElementId(node.parent)] = node; - }, + return { + JSXAttribute: function(node) { + lastAttributeNode[getOpeningElementId(node.parent)] = node; + }, - JSXSpreadAttribute: function(node) { - lastAttributeNode[getOpeningElementId(node.parent)] = node; - }, + JSXSpreadAttribute: function(node) { + lastAttributeNode[getOpeningElementId(node.parent)] = node; + }, - 'JSXOpeningElement:exit': function(node) { - var attributeNode = lastAttributeNode[getOpeningElementId(node)]; - var cachedLastAttributeEndPos = attributeNode ? attributeNode.end : null; - var expectedNextLine; - var tokens = getTokensLocations(node); - var expectedLocation = getExpectedLocation(tokens); + 'JSXOpeningElement:exit': function(node) { + var attributeNode = lastAttributeNode[getOpeningElementId(node)]; + var cachedLastAttributeEndPos = attributeNode ? attributeNode.end : null; + var expectedNextLine; + var tokens = getTokensLocations(node); + var expectedLocation = getExpectedLocation(tokens); - if (hasCorrectLocation(tokens, expectedLocation)) { - return; - } + if (hasCorrectLocation(tokens, expectedLocation)) { + return; + } - var data = {location: MESSAGE_LOCATION[expectedLocation], details: ''}; - var correctColumn = getCorrectColumn(tokens, expectedLocation); + var data = {location: MESSAGE_LOCATION[expectedLocation], details: ''}; + var correctColumn = getCorrectColumn(tokens, expectedLocation); - if (correctColumn !== null) { - expectedNextLine = tokens.lastProp && - (tokens.lastProp.lastLine === tokens.closing.line); - data.details = ' (expected column ' + (correctColumn + 1) + - (expectedNextLine ? ' on the next line)' : ')'); - } + if (correctColumn !== null) { + expectedNextLine = tokens.lastProp && + (tokens.lastProp.lastLine === tokens.closing.line); + data.details = ' (expected column ' + (correctColumn + 1) + + (expectedNextLine ? ' on the next line)' : ')'); + } - context.report({ - node: node, - loc: tokens.closing, - message: MESSAGE, - data: data, - fix: function(fixer) { - var closingTag = tokens.selfClosing ? '/>' : '>'; - switch (expectedLocation) { - case 'after-tag': - if (cachedLastAttributeEndPos) { + context.report({ + node: node, + loc: tokens.closing, + message: MESSAGE, + data: data, + fix: function(fixer) { + var closingTag = tokens.selfClosing ? '/>' : '>'; + switch (expectedLocation) { + case 'after-tag': + if (cachedLastAttributeEndPos) { + return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end], + (expectedNextLine ? '\n' : '') + closingTag); + } + return fixer.replaceTextRange([node.name.range[1], node.end], + (expectedNextLine ? '\n' : ' ') + closingTag); + case 'after-props': return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end], (expectedNextLine ? '\n' : '') + closingTag); - } - return fixer.replaceTextRange([node.name.range[1], node.end], - (expectedNextLine ? '\n' : ' ') + closingTag); - case 'after-props': - return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end], - (expectedNextLine ? '\n' : '') + closingTag); - case 'props-aligned': - case 'tag-aligned': - case 'line-aligned': - var spaces = new Array(+correctColumn + 1); - return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end], - '\n' + spaces.join(' ') + closingTag); - default: - return true; + case 'props-aligned': + case 'tag-aligned': + case 'line-aligned': + var spaces = new Array(+correctColumn + 1); + return fixer.replaceTextRange([cachedLastAttributeEndPos, node.end], + '\n' + spaces.join(' ') + closingTag); + default: + return true; + } } - } - }); - } - }; + }); + } + }; + } }; - -module.exports.schema = [{ - oneOf: [ - { - enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'] - }, - { - type: 'object', - properties: { - location: { - enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned'] - } - }, - additionalProperties: false - }, { - type: 'object', - properties: { - nonEmpty: { - enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false] - }, - selfClosing: { - enum: ['after-props', 'props-aligned', 'tag-aligned', 'line-aligned', false] - } - }, - additionalProperties: false - } - ] -}]; diff --git a/lib/rules/jsx-curly-spacing.js b/lib/rules/jsx-curly-spacing.js index e42a04a66d..3b625804d0 100644 --- a/lib/rules/jsx-curly-spacing.js +++ b/lib/rules/jsx-curly-spacing.js @@ -20,231 +20,238 @@ var SPACING = { }; var SPACING_VALUES = [SPACING.always, SPACING.never]; -module.exports = function(context) { - - var sourceCode = context.getSourceCode(); - var spaced = context.options[0] === SPACING.always; - var multiline = context.options[1] ? context.options[1].allowMultiline : true; - var spacing = context.options[1] ? context.options[1].spacing || {} : {}; - var defaultSpacing = spaced ? SPACING.always : SPACING.never; - var objectLiteralSpacing = spacing.objectLiterals || (spaced ? SPACING.always : SPACING.never); - - // -------------------------------------------------------------------------- - // Helpers - // -------------------------------------------------------------------------- - - /** - * Determines whether two adjacent tokens have a newline between them. - * @param {Object} left - The left token object. - * @param {Object} right - The right token object. - * @returns {boolean} Whether or not there is a newline between the tokens. - */ - function isMultiline(left, right) { - return left.loc.start.line !== right.loc.start.line; - } +module.exports = { + meta: { + docs: {}, + fixable: 'code', - /** - * Reports that there shouldn't be a newline after the first token - * @param {ASTNode} node - The node to report in the event of an error. - * @param {Token} token - The token to use for the report. - * @returns {void} - */ - function reportNoBeginningNewline(node, token) { - context.report({ - node: node, - loc: token.loc.start, - message: 'There should be no newline after \'' + token.value + '\'', - fix: function(fixer) { - var nextToken = context.getSourceCode().getTokenAfter(token); - return fixer.replaceTextRange([token.range[1], nextToken.range[0]], spaced ? ' ' : ''); - } - }); - } + schema: [{ + enum: SPACING_VALUES + }, { + type: 'object', + properties: { + allowMultiline: { + type: 'boolean' + }, + spacing: { + type: 'object', + properties: { + objectLiterals: { + enum: SPACING_VALUES + } + } + } + }, + additionalProperties: false + }] + }, - /** - * Reports that there shouldn't be a newline before the last token - * @param {ASTNode} node - The node to report in the event of an error. - * @param {Token} token - The token to use for the report. - * @returns {void} - */ - function reportNoEndingNewline(node, token) { - context.report({ - node: node, - loc: token.loc.start, - message: 'There should be no newline before \'' + token.value + '\'', - fix: function(fixer) { - var previousToken = context.getSourceCode().getTokenBefore(token); - return fixer.replaceTextRange([previousToken.range[1], token.range[0]], spaced ? ' ' : ''); - } - }); - } + create: function(context) { - /** - * Reports that there shouldn't be a space after the first token - * @param {ASTNode} node - The node to report in the event of an error. - * @param {Token} token - The token to use for the report. - * @returns {void} - */ - function reportNoBeginningSpace(node, token) { - context.report({ - node: node, - loc: token.loc.start, - message: 'There should be no space after \'' + token.value + '\'', - fix: function(fixer) { - var nextToken = context.getSourceCode().getTokenAfter(token); - return fixer.removeRange([token.range[1], nextToken.range[0]]); - } - }); - } + var sourceCode = context.getSourceCode(); + var spaced = context.options[0] === SPACING.always; + var multiline = context.options[1] ? context.options[1].allowMultiline : true; + var spacing = context.options[1] ? context.options[1].spacing || {} : {}; + var defaultSpacing = spaced ? SPACING.always : SPACING.never; + var objectLiteralSpacing = spacing.objectLiterals || (spaced ? SPACING.always : SPACING.never); - /** - * Reports that there shouldn't be a space before the last token - * @param {ASTNode} node - The node to report in the event of an error. - * @param {Token} token - The token to use for the report. - * @returns {void} - */ - function reportNoEndingSpace(node, token) { - context.report({ - node: node, - loc: token.loc.start, - message: 'There should be no space before \'' + token.value + '\'', - fix: function(fixer) { - var previousToken = context.getSourceCode().getTokenBefore(token); - return fixer.removeRange([previousToken.range[1], token.range[0]]); - } - }); - } + // -------------------------------------------------------------------------- + // Helpers + // -------------------------------------------------------------------------- - /** - * Reports that there should be a space after the first token - * @param {ASTNode} node - The node to report in the event of an error. - * @param {Token} token - The token to use for the report. - * @returns {void} - */ - function reportRequiredBeginningSpace(node, token) { - context.report({ - node: node, - loc: token.loc.start, - message: 'A space is required after \'' + token.value + '\'', - fix: function(fixer) { - return fixer.insertTextAfter(token, ' '); - } - }); - } + /** + * Determines whether two adjacent tokens have a newline between them. + * @param {Object} left - The left token object. + * @param {Object} right - The right token object. + * @returns {boolean} Whether or not there is a newline between the tokens. + */ + function isMultiline(left, right) { + return left.loc.start.line !== right.loc.start.line; + } - /** - * Reports that there should be a space before the last token - * @param {ASTNode} node - The node to report in the event of an error. - * @param {Token} token - The token to use for the report. - * @returns {void} - */ - function reportRequiredEndingSpace(node, token) { - context.report({ - node: node, - loc: token.loc.start, - message: 'A space is required before \'' + token.value + '\'', - fix: function(fixer) { - return fixer.insertTextBefore(token, ' '); - } - }); - } + /** + * Reports that there shouldn't be a newline after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoBeginningNewline(node, token) { + context.report({ + node: node, + loc: token.loc.start, + message: 'There should be no newline after \'' + token.value + '\'', + fix: function(fixer) { + var nextToken = context.getSourceCode().getTokenAfter(token); + return fixer.replaceTextRange([token.range[1], nextToken.range[0]], spaced ? ' ' : ''); + } + }); + } - /** - * Determines if spacing in curly braces is valid. - * @param {ASTNode} node The AST node to check. - * @returns {void} - */ - function validateBraceSpacing(node) { - // Only validate attributes - if (node.parent.type === 'JSXElement') { - return; + /** + * Reports that there shouldn't be a newline before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoEndingNewline(node, token) { + context.report({ + node: node, + loc: token.loc.start, + message: 'There should be no newline before \'' + token.value + '\'', + fix: function(fixer) { + var previousToken = context.getSourceCode().getTokenBefore(token); + return fixer.replaceTextRange([previousToken.range[1], token.range[0]], spaced ? ' ' : ''); + } + }); } - var first = context.getFirstToken(node); - var last = sourceCode.getLastToken(node); - var second = context.getTokenAfter(first); - var penultimate = sourceCode.getTokenBefore(last); - - var isObjectLiteral = first.value === second.value; - if (isObjectLiteral) { - if (objectLiteralSpacing === SPACING.never) { - if (sourceCode.isSpaceBetweenTokens(first, second)) { - reportNoBeginningSpace(node, first); - } else if (!multiline && isMultiline(first, second)) { - reportNoBeginningNewline(node, first); + + /** + * Reports that there shouldn't be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoBeginningSpace(node, token) { + context.report({ + node: node, + loc: token.loc.start, + message: 'There should be no space after \'' + token.value + '\'', + fix: function(fixer) { + var nextToken = context.getSourceCode().getTokenAfter(token); + return fixer.removeRange([token.range[1], nextToken.range[0]]); } - if (sourceCode.isSpaceBetweenTokens(penultimate, last)) { - reportNoEndingSpace(node, last); - } else if (!multiline && isMultiline(penultimate, last)) { - reportNoEndingNewline(node, last); + }); + } + + /** + * Reports that there shouldn't be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportNoEndingSpace(node, token) { + context.report({ + node: node, + loc: token.loc.start, + message: 'There should be no space before \'' + token.value + '\'', + fix: function(fixer) { + var previousToken = context.getSourceCode().getTokenBefore(token); + return fixer.removeRange([previousToken.range[1], token.range[0]]); } - } else if (objectLiteralSpacing === SPACING.always) { + }); + } + + /** + * Reports that there should be a space after the first token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredBeginningSpace(node, token) { + context.report({ + node: node, + loc: token.loc.start, + message: 'A space is required after \'' + token.value + '\'', + fix: function(fixer) { + return fixer.insertTextAfter(token, ' '); + } + }); + } + + /** + * Reports that there should be a space before the last token + * @param {ASTNode} node - The node to report in the event of an error. + * @param {Token} token - The token to use for the report. + * @returns {void} + */ + function reportRequiredEndingSpace(node, token) { + context.report({ + node: node, + loc: token.loc.start, + message: 'A space is required before \'' + token.value + '\'', + fix: function(fixer) { + return fixer.insertTextBefore(token, ' '); + } + }); + } + + /** + * Determines if spacing in curly braces is valid. + * @param {ASTNode} node The AST node to check. + * @returns {void} + */ + function validateBraceSpacing(node) { + // Only validate attributes + if (node.parent.type === 'JSXElement') { + return; + } + var first = context.getFirstToken(node); + var last = sourceCode.getLastToken(node); + var second = context.getTokenAfter(first); + var penultimate = sourceCode.getTokenBefore(last); + + var isObjectLiteral = first.value === second.value; + if (isObjectLiteral) { + if (objectLiteralSpacing === SPACING.never) { + if (sourceCode.isSpaceBetweenTokens(first, second)) { + reportNoBeginningSpace(node, first); + } else if (!multiline && isMultiline(first, second)) { + reportNoBeginningNewline(node, first); + } + if (sourceCode.isSpaceBetweenTokens(penultimate, last)) { + reportNoEndingSpace(node, last); + } else if (!multiline && isMultiline(penultimate, last)) { + reportNoEndingNewline(node, last); + } + } else if (objectLiteralSpacing === SPACING.always) { + if (!sourceCode.isSpaceBetweenTokens(first, second)) { + reportRequiredBeginningSpace(node, first); + } else if (!multiline && isMultiline(first, second)) { + reportNoBeginningNewline(node, first); + } + if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) { + reportRequiredEndingSpace(node, last); + } else if (!multiline && isMultiline(penultimate, last)) { + reportNoEndingNewline(node, last); + } + } + } else if (defaultSpacing === SPACING.always) { if (!sourceCode.isSpaceBetweenTokens(first, second)) { reportRequiredBeginningSpace(node, first); } else if (!multiline && isMultiline(first, second)) { reportNoBeginningNewline(node, first); } + if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) { reportRequiredEndingSpace(node, last); } else if (!multiline && isMultiline(penultimate, last)) { reportNoEndingNewline(node, last); } - } - } else if (defaultSpacing === SPACING.always) { - if (!sourceCode.isSpaceBetweenTokens(first, second)) { - reportRequiredBeginningSpace(node, first); - } else if (!multiline && isMultiline(first, second)) { - reportNoBeginningNewline(node, first); - } - - if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) { - reportRequiredEndingSpace(node, last); - } else if (!multiline && isMultiline(penultimate, last)) { - reportNoEndingNewline(node, last); - } - } else if (defaultSpacing === SPACING.never) { - if (isMultiline(first, second)) { - if (!multiline) { - reportNoBeginningNewline(node, first); + } else if (defaultSpacing === SPACING.never) { + if (isMultiline(first, second)) { + if (!multiline) { + reportNoBeginningNewline(node, first); + } + } else if (sourceCode.isSpaceBetweenTokens(first, second)) { + reportNoBeginningSpace(node, first); } - } else if (sourceCode.isSpaceBetweenTokens(first, second)) { - reportNoBeginningSpace(node, first); - } - if (isMultiline(penultimate, last)) { - if (!multiline) { - reportNoEndingNewline(node, last); + if (isMultiline(penultimate, last)) { + if (!multiline) { + reportNoEndingNewline(node, last); + } + } else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) { + reportNoEndingSpace(node, last); } - } else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) { - reportNoEndingSpace(node, last); } } - } - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - return { - JSXExpressionContainer: validateBraceSpacing, - JSXSpreadAttribute: validateBraceSpacing - }; + return { + JSXExpressionContainer: validateBraceSpacing, + JSXSpreadAttribute: validateBraceSpacing + }; + } }; - -module.exports.schema = [{ - enum: SPACING_VALUES -}, { - type: 'object', - properties: { - allowMultiline: { - type: 'boolean' - }, - spacing: { - type: 'object', - properties: { - objectLiterals: { - enum: SPACING_VALUES - } - } - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-equals-spacing.js b/lib/rules/jsx-equals-spacing.js index 579088e995..6b6aab85fb 100644 --- a/lib/rules/jsx-equals-spacing.js +++ b/lib/rules/jsx-equals-spacing.js @@ -8,86 +8,93 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - var config = context.options[0]; - var sourceCode = context.getSourceCode(); +module.exports = { + meta: { + docs: {}, + fixable: 'code', - /** - * Determines a given attribute node has an equal sign. - * @param {ASTNode} attrNode - The attribute node. - * @returns {boolean} Whether or not the attriute node has an equal sign. - */ - function hasEqual(attrNode) { - return attrNode.type !== 'JSXSpreadAttribute' && attrNode.value !== null; - } + schema: [{ + enum: ['always', 'never'] + }] + }, - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + create: function(context) { + var config = context.options[0]; + var sourceCode = context.getSourceCode(); - return { - JSXOpeningElement: function(node) { - node.attributes.forEach(function(attrNode) { - if (!hasEqual(attrNode)) { - return; - } + /** + * Determines a given attribute node has an equal sign. + * @param {ASTNode} attrNode - The attribute node. + * @returns {boolean} Whether or not the attriute node has an equal sign. + */ + function hasEqual(attrNode) { + return attrNode.type !== 'JSXSpreadAttribute' && attrNode.value !== null; + } - var equalToken = sourceCode.getTokenAfter(attrNode.name); - var spacedBefore = sourceCode.isSpaceBetweenTokens(attrNode.name, equalToken); - var spacedAfter = sourceCode.isSpaceBetweenTokens(equalToken, attrNode.value); + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - switch (config) { - default: - case 'never': - if (spacedBefore) { - context.report({ - node: attrNode, - loc: equalToken.loc.start, - message: 'There should be no space before \'=\'', - fix: function(fixer) { - return fixer.removeRange([attrNode.name.range[1], equalToken.start]); - } - }); - } - if (spacedAfter) { - context.report({ - node: attrNode, - loc: equalToken.loc.start, - message: 'There should be no space after \'=\'', - fix: function(fixer) { - return fixer.removeRange([equalToken.end, attrNode.value.range[0]]); - } - }); - } - break; - case 'always': - if (!spacedBefore) { - context.report({ - node: attrNode, - loc: equalToken.loc.start, - message: 'A space is required before \'=\'', - fix: function(fixer) { - return fixer.insertTextBefore(equalToken, ' '); - } - }); - } - if (!spacedAfter) { - context.report({ - node: attrNode, - loc: equalToken.loc.start, - message: 'A space is required after \'=\'', - fix: function(fixer) { - return fixer.insertTextAfter(equalToken, ' '); - } - }); - } - break; - } - }); - } - }; -}; + return { + JSXOpeningElement: function(node) { + node.attributes.forEach(function(attrNode) { + if (!hasEqual(attrNode)) { + return; + } -module.exports.schema = [{ - enum: ['always', 'never'] -}]; + var equalToken = sourceCode.getTokenAfter(attrNode.name); + var spacedBefore = sourceCode.isSpaceBetweenTokens(attrNode.name, equalToken); + var spacedAfter = sourceCode.isSpaceBetweenTokens(equalToken, attrNode.value); + + switch (config) { + default: + case 'never': + if (spacedBefore) { + context.report({ + node: attrNode, + loc: equalToken.loc.start, + message: 'There should be no space before \'=\'', + fix: function(fixer) { + return fixer.removeRange([attrNode.name.range[1], equalToken.start]); + } + }); + } + if (spacedAfter) { + context.report({ + node: attrNode, + loc: equalToken.loc.start, + message: 'There should be no space after \'=\'', + fix: function(fixer) { + return fixer.removeRange([equalToken.end, attrNode.value.range[0]]); + } + }); + } + break; + case 'always': + if (!spacedBefore) { + context.report({ + node: attrNode, + loc: equalToken.loc.start, + message: 'A space is required before \'=\'', + fix: function(fixer) { + return fixer.insertTextBefore(equalToken, ' '); + } + }); + } + if (!spacedAfter) { + context.report({ + node: attrNode, + loc: equalToken.loc.start, + message: 'A space is required after \'=\'', + fix: function(fixer) { + return fixer.insertTextAfter(equalToken, ' '); + } + }); + } + break; + } + }); + } + }; + } +}; diff --git a/lib/rules/jsx-filename-extension.js b/lib/rules/jsx-filename-extension.js index 727a3c1601..94566445ad 100644 --- a/lib/rules/jsx-filename-extension.js +++ b/lib/rules/jsx-filename-extension.js @@ -18,67 +18,73 @@ var DEFAULTS = { // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - - - function getExtensionsConfig() { - return context.options[0] && context.options[0].extensions || DEFAULTS.extensions; - } - - var invalidExtension; - var invalidNode; - - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: { + extensions: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + }] + }, - return { - JSXElement: function(node) { - var filename = context.getFilename(); - if (filename === '') { - return; - } + create: function(context) { - if (invalidNode) { - return; - } - var allowedExtensions = getExtensionsConfig(); - var isAllowedExtension = allowedExtensions.some(function (extension) { - return filename.slice(-extension.length) === extension; - }); - - if (isAllowedExtension) { - return; - } - - invalidNode = node; - invalidExtension = path.extname(filename); - }, + function getExtensionsConfig() { + return context.options[0] && context.options[0].extensions || DEFAULTS.extensions; + } - 'Program:exit': function() { - if (!invalidNode) { - return; + var invalidExtension; + var invalidNode; + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + JSXElement: function(node) { + var filename = context.getFilename(); + if (filename === '') { + return; + } + + if (invalidNode) { + return; + } + + var allowedExtensions = getExtensionsConfig(); + var isAllowedExtension = allowedExtensions.some(function (extension) { + return filename.slice(-extension.length) === extension; + }); + + if (isAllowedExtension) { + return; + } + + invalidNode = node; + invalidExtension = path.extname(filename); + }, + + 'Program:exit': function() { + if (!invalidNode) { + return; + } + + context.report({ + node: invalidNode, + message: 'JSX not allowed in files with extension \'' + invalidExtension + '\'' + }); } + }; - context.report({ - node: invalidNode, - message: 'JSX not allowed in files with extension \'' + invalidExtension + '\'' - }); - } - }; - + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - extensions: { - type: 'array', - items: { - type: 'string' - } - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-first-prop-new-line.js b/lib/rules/jsx-first-prop-new-line.js index d9fb0502f8..7af4209832 100644 --- a/lib/rules/jsx-first-prop-new-line.js +++ b/lib/rules/jsx-first-prop-new-line.js @@ -8,39 +8,45 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function (context) { - var configuration = context.options[0]; +module.exports = { + meta: { + docs: {}, - function isMultilineJSX(jsxNode) { - return jsxNode.loc.start.line < jsxNode.loc.end.line; - } + schema: [{ + enum: ['always', 'never', 'multiline'] + }] + }, + + create: function (context) { + var configuration = context.options[0]; - return { - JSXOpeningElement: function (node) { - if ((configuration === 'multiline' && isMultilineJSX(node)) || (configuration === 'always')) { - node.attributes.forEach(function(decl) { - if (decl.loc.start.line === node.loc.start.line) { + function isMultilineJSX(jsxNode) { + return jsxNode.loc.start.line < jsxNode.loc.end.line; + } + + return { + JSXOpeningElement: function (node) { + if ((configuration === 'multiline' && isMultilineJSX(node)) || (configuration === 'always')) { + node.attributes.forEach(function(decl) { + if (decl.loc.start.line === node.loc.start.line) { + context.report({ + node: decl, + message: 'Property should be placed on a new line' + }); + } + }); + } else if (configuration === 'never' && node.attributes.length > 0) { + var firstNode = node.attributes[0]; + if (node.loc.start.line < firstNode.loc.start.line) { context.report({ - node: decl, - message: 'Property should be placed on a new line' + node: firstNode, + message: 'Property should be placed on the same line as the component declaration' }); + return; } - }); - } else if (configuration === 'never' && node.attributes.length > 0) { - var firstNode = node.attributes[0]; - if (node.loc.start.line < firstNode.loc.start.line) { - context.report({ - node: firstNode, - message: 'Property should be placed on the same line as the component declaration' - }); - return; } + return; } - return; - } - }; + }; + } }; - -module.exports.schema = [{ - enum: ['always', 'never', 'multiline'] -}]; diff --git a/lib/rules/jsx-handler-names.js b/lib/rules/jsx-handler-names.js index 56d35e07e6..adefe38c61 100644 --- a/lib/rules/jsx-handler-names.js +++ b/lib/rules/jsx-handler-names.js @@ -8,58 +8,64 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, - var sourceCode = context.getSourceCode(); - var configuration = context.options[0] || {}; - var eventHandlerPrefix = configuration.eventHandlerPrefix || 'handle'; - var eventHandlerPropPrefix = configuration.eventHandlerPropPrefix || 'on'; + schema: [{ + type: 'object', + properties: { + eventHandlerPrefix: { + type: 'string' + }, + eventHandlerPropPrefix: { + type: 'string' + } + }, + additionalProperties: false + }] + }, - var EVENT_HANDLER_REGEX = new RegExp('^((props\\.' + eventHandlerPropPrefix + ')' - + '|((.*\\.)?' + eventHandlerPrefix + ')).+$'); - var PROP_EVENT_HANDLER_REGEX = new RegExp('^(' + eventHandlerPropPrefix + '.+|ref)$'); + create: function(context) { - return { - JSXAttribute: function(node) { - if (!node.value || !node.value.expression || !node.value.expression.object) { - return; - } + var sourceCode = context.getSourceCode(); + var configuration = context.options[0] || {}; + var eventHandlerPrefix = configuration.eventHandlerPrefix || 'handle'; + var eventHandlerPropPrefix = configuration.eventHandlerPropPrefix || 'on'; - var propKey = typeof node.name === 'object' ? node.name.name : node.name; - var propValue = sourceCode.getText(node.value.expression).replace(/^this\.|.*::/, ''); + var EVENT_HANDLER_REGEX = new RegExp('^((props\\.' + eventHandlerPropPrefix + ')' + + '|((.*\\.)?' + eventHandlerPrefix + ')).+$'); + var PROP_EVENT_HANDLER_REGEX = new RegExp('^(' + eventHandlerPropPrefix + '.+|ref)$'); - if (propKey === 'ref') { - return; - } + return { + JSXAttribute: function(node) { + if (!node.value || !node.value.expression || !node.value.expression.object) { + return; + } + + var propKey = typeof node.name === 'object' ? node.name.name : node.name; + var propValue = sourceCode.getText(node.value.expression).replace(/^this\.|.*::/, ''); - var propIsEventHandler = PROP_EVENT_HANDLER_REGEX.test(propKey); - var propFnIsNamedCorrectly = EVENT_HANDLER_REGEX.test(propValue); + if (propKey === 'ref') { + return; + } - if (propIsEventHandler && !propFnIsNamedCorrectly) { - context.report({ - node: node, - message: 'Handler function for ' + propKey + ' prop key must begin with \'' + eventHandlerPrefix + '\'' - }); - } else if (propFnIsNamedCorrectly && !propIsEventHandler) { - context.report({ - node: node, - message: 'Prop key for ' + propValue + ' must begin with \'' + eventHandlerPropPrefix + '\'' - }); + var propIsEventHandler = PROP_EVENT_HANDLER_REGEX.test(propKey); + var propFnIsNamedCorrectly = EVENT_HANDLER_REGEX.test(propValue); + + if (propIsEventHandler && !propFnIsNamedCorrectly) { + context.report({ + node: node, + message: 'Handler function for ' + propKey + ' prop key must begin with \'' + eventHandlerPrefix + '\'' + }); + } else if (propFnIsNamedCorrectly && !propIsEventHandler) { + context.report({ + node: node, + message: 'Prop key for ' + propValue + ' must begin with \'' + eventHandlerPropPrefix + '\'' + }); + } } - } - }; + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - eventHandlerPrefix: { - type: 'string' - }, - eventHandlerPropPrefix: { - type: 'string' - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-indent-props.js b/lib/rules/jsx-indent-props.js index bf486f4d33..ce3abdd107 100644 --- a/lib/rules/jsx-indent-props.js +++ b/lib/rules/jsx-indent-props.js @@ -32,138 +32,145 @@ // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - - var MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.'; +module.exports = { + meta: { + docs: {}, + fixable: 'code', + + schema: [{ + oneOf: [{ + enum: ['tab'] + }, { + type: 'integer' + }] + }] + }, + + create: function(context) { + + var MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.'; + + var extraColumnStart = 0; + var indentType = 'space'; + var indentSize = 4; + + var sourceCode = context.getSourceCode(); + + if (context.options.length) { + if (context.options[0] === 'tab') { + indentSize = 1; + indentType = 'tab'; + } else if (typeof context.options[0] === 'number') { + indentSize = context.options[0]; + indentType = 'space'; + } + } - var extraColumnStart = 0; - var indentType = 'space'; - var indentSize = 4; + /** + * Reports a given indent violation and properly pluralizes the message + * @param {ASTNode} node Node violating the indent rule + * @param {Number} needed Expected indentation character count + * @param {Number} gotten Indentation character count in the actual node/code + * @param {Object=} loc Error line and column location + */ + function report(node, needed, gotten, loc) { + var msgContext = { + needed: needed, + type: indentType, + characters: needed === 1 ? 'character' : 'characters', + gotten: gotten + }; + + if (loc) { + context.report({ + node: node, + loc: loc, + message: MESSAGE, + data: msgContext + }); + } else { + context.report({ + node: node, + message: MESSAGE, + data: msgContext, + fix: function(fixer) { + return fixer.replaceTextRange([node.start - node.loc.start.column, node.start], + Array(needed + 1).join(indentType === 'space' ? ' ' : '\t')); + } + }); + } + } - var sourceCode = context.getSourceCode(); + /** + * Get node indent + * @param {ASTNode} node Node to examine + * @param {Boolean} byLastLine get indent of node's last line + * @param {Boolean} excludeCommas skip comma on start of line + * @return {Number} Indent + */ + function getNodeIndent(node, byLastLine, excludeCommas) { + byLastLine = byLastLine || false; + excludeCommas = excludeCommas || false; + + var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart); + var lines = src.split('\n'); + if (byLastLine) { + src = lines[lines.length - 1]; + } else { + src = lines[0]; + } - if (context.options.length) { - if (context.options[0] === 'tab') { - indentSize = 1; - indentType = 'tab'; - } else if (typeof context.options[0] === 'number') { - indentSize = context.options[0]; - indentType = 'space'; - } - } + var skip = excludeCommas ? ',' : ''; - /** - * Reports a given indent violation and properly pluralizes the message - * @param {ASTNode} node Node violating the indent rule - * @param {Number} needed Expected indentation character count - * @param {Number} gotten Indentation character count in the actual node/code - * @param {Object=} loc Error line and column location - */ - function report(node, needed, gotten, loc) { - var msgContext = { - needed: needed, - type: indentType, - characters: needed === 1 ? 'character' : 'characters', - gotten: gotten - }; + var regExp; + if (indentType === 'space') { + regExp = new RegExp('^[ ' + skip + ']+'); + } else { + regExp = new RegExp('^[\t' + skip + ']+'); + } - if (loc) { - context.report({ - node: node, - loc: loc, - message: MESSAGE, - data: msgContext - }); - } else { - context.report({ - node: node, - message: MESSAGE, - data: msgContext, - fix: function(fixer) { - return fixer.replaceTextRange([node.start - node.loc.start.column, node.start], - Array(needed + 1).join(indentType === 'space' ? ' ' : '\t')); - } - }); + var indent = regExp.exec(src); + return indent ? indent[0].length : 0; } - } - /** - * Get node indent - * @param {ASTNode} node Node to examine - * @param {Boolean} byLastLine get indent of node's last line - * @param {Boolean} excludeCommas skip comma on start of line - * @return {Number} Indent - */ - function getNodeIndent(node, byLastLine, excludeCommas) { - byLastLine = byLastLine || false; - excludeCommas = excludeCommas || false; - - var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart); - var lines = src.split('\n'); - if (byLastLine) { - src = lines[lines.length - 1]; - } else { - src = lines[0]; + /** + * Checks node is the first in its own start line. By default it looks by start line. + * @param {ASTNode} node The node to check + * @param {Boolean} [byEndLocation] Lookup based on start position or end + * @return {Boolean} true if its the first in the its start line + */ + function isNodeFirstInLine(node, byEndLocation) { + var firstToken = byEndLocation === true ? sourceCode.getLastToken(node, 1) : sourceCode.getTokenBefore(node); + var startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line; + var endLine = firstToken ? firstToken.loc.end.line : -1; + + return startLine !== endLine; } - var skip = excludeCommas ? ',' : ''; - - var regExp; - if (indentType === 'space') { - regExp = new RegExp('^[ ' + skip + ']+'); - } else { - regExp = new RegExp('^[\t' + skip + ']+'); + /** + * Check indent for nodes list + * @param {ASTNode[]} nodes list of node objects + * @param {Number} indent needed indent + * @param {Boolean} excludeCommas skip comma on start of line + */ + function checkNodesIndent(nodes, indent, excludeCommas) { + nodes.forEach(function(node) { + var nodeIndent = getNodeIndent(node, false, excludeCommas); + if ( + node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' && + nodeIndent !== indent && isNodeFirstInLine(node) + ) { + report(node, indent, nodeIndent); + } + }); } - var indent = regExp.exec(src); - return indent ? indent[0].length : 0; - } - - /** - * Checks node is the first in its own start line. By default it looks by start line. - * @param {ASTNode} node The node to check - * @param {Boolean} [byEndLocation] Lookup based on start position or end - * @return {Boolean} true if its the first in the its start line - */ - function isNodeFirstInLine(node, byEndLocation) { - var firstToken = byEndLocation === true ? sourceCode.getLastToken(node, 1) : sourceCode.getTokenBefore(node); - var startLine = byEndLocation === true ? node.loc.end.line : node.loc.start.line; - var endLine = firstToken ? firstToken.loc.end.line : -1; - - return startLine !== endLine; - } - - /** - * Check indent for nodes list - * @param {ASTNode[]} nodes list of node objects - * @param {Number} indent needed indent - * @param {Boolean} excludeCommas skip comma on start of line - */ - function checkNodesIndent(nodes, indent, excludeCommas) { - nodes.forEach(function(node) { - var nodeIndent = getNodeIndent(node, false, excludeCommas); - if ( - node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression' && - nodeIndent !== indent && isNodeFirstInLine(node) - ) { - report(node, indent, nodeIndent); + return { + JSXOpeningElement: function(node) { + var elementIndent = getNodeIndent(node); + checkNodesIndent(node.attributes, elementIndent + indentSize); } - }); - } - - return { - JSXOpeningElement: function(node) { - var elementIndent = getNodeIndent(node); - checkNodesIndent(node.attributes, elementIndent + indentSize); - } - }; + }; + } }; - -module.exports.schema = [{ - oneOf: [{ - enum: ['tab'] - }, { - type: 'integer' - }] -}]; diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js index 8b8394ee9e..0caa4ebfc4 100644 --- a/lib/rules/jsx-indent.js +++ b/lib/rules/jsx-indent.js @@ -32,142 +32,148 @@ // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - - var MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.'; - - var extraColumnStart = 0; - var indentType = 'space'; - var indentSize = 4; - - var sourceCode = context.getSourceCode(); - - if (context.options.length) { - if (context.options[0] === 'tab') { - indentSize = 1; - indentType = 'tab'; - } else if (typeof context.options[0] === 'number') { - indentSize = context.options[0]; - indentType = 'space'; +module.exports = { + meta: { + docs: {}, + + schema: [{ + oneOf: [{ + enum: ['tab'] + }, { + type: 'integer' + }] + }] + }, + + create: function(context) { + + var MESSAGE = 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.'; + + var extraColumnStart = 0; + var indentType = 'space'; + var indentSize = 4; + + var sourceCode = context.getSourceCode(); + + if (context.options.length) { + if (context.options[0] === 'tab') { + indentSize = 1; + indentType = 'tab'; + } else if (typeof context.options[0] === 'number') { + indentSize = context.options[0]; + indentType = 'space'; + } } - } - /** - * Reports a given indent violation and properly pluralizes the message - * @param {ASTNode} node Node violating the indent rule - * @param {Number} needed Expected indentation character count - * @param {Number} gotten Indentation character count in the actual node/code - * @param {Object=} loc Error line and column location - */ - function report(node, needed, gotten, loc) { - var msgContext = { - needed: needed, - type: indentType, - characters: needed === 1 ? 'character' : 'characters', - gotten: gotten - }; - - if (loc) { - context.report({ - node: node, - loc: loc, - message: MESSAGE, - data: msgContext - }); - } else { - context.report({ - node: node, - message: MESSAGE, - data: msgContext - }); + /** + * Reports a given indent violation and properly pluralizes the message + * @param {ASTNode} node Node violating the indent rule + * @param {Number} needed Expected indentation character count + * @param {Number} gotten Indentation character count in the actual node/code + * @param {Object=} loc Error line and column location + */ + function report(node, needed, gotten, loc) { + var msgContext = { + needed: needed, + type: indentType, + characters: needed === 1 ? 'character' : 'characters', + gotten: gotten + }; + + if (loc) { + context.report({ + node: node, + loc: loc, + message: MESSAGE, + data: msgContext + }); + } else { + context.report({ + node: node, + message: MESSAGE, + data: msgContext + }); + } } - } - /** - * Get node indent - * @param {ASTNode} node Node to examine - * @param {Boolean} byLastLine get indent of node's last line - * @param {Boolean} excludeCommas skip comma on start of line - * @return {Number} Indent - */ - function getNodeIndent(node, byLastLine, excludeCommas) { - byLastLine = byLastLine || false; - excludeCommas = excludeCommas || false; - - var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart); - var lines = src.split('\n'); - if (byLastLine) { - src = lines[lines.length - 1]; - } else { - src = lines[0]; - } + /** + * Get node indent + * @param {ASTNode} node Node to examine + * @param {Boolean} byLastLine get indent of node's last line + * @param {Boolean} excludeCommas skip comma on start of line + * @return {Number} Indent + */ + function getNodeIndent(node, byLastLine, excludeCommas) { + byLastLine = byLastLine || false; + excludeCommas = excludeCommas || false; + + var src = sourceCode.getText(node, node.loc.start.column + extraColumnStart); + var lines = src.split('\n'); + if (byLastLine) { + src = lines[lines.length - 1]; + } else { + src = lines[0]; + } - var skip = excludeCommas ? ',' : ''; + var skip = excludeCommas ? ',' : ''; - var regExp; - if (indentType === 'space') { - regExp = new RegExp('^[ ' + skip + ']+'); - } else { - regExp = new RegExp('^[\t' + skip + ']+'); - } - - var indent = regExp.exec(src); - return indent ? indent[0].length : 0; - } + var regExp; + if (indentType === 'space') { + regExp = new RegExp('^[ ' + skip + ']+'); + } else { + regExp = new RegExp('^[\t' + skip + ']+'); + } - /** - * Checks node is the first in its own start line. By default it looks by start line. - * @param {ASTNode} node The node to check - * @return {Boolean} true if its the first in the its start line - */ - function isNodeFirstInLine(node) { - var token = node; - do { - token = sourceCode.getTokenBefore(token); - } while (token.type === 'JSXText'); - var startLine = node.loc.start.line; - var endLine = token ? token.loc.end.line : -1; - - return startLine !== endLine; - } + var indent = regExp.exec(src); + return indent ? indent[0].length : 0; + } - /** - * Check indent for nodes list - * @param {ASTNode[]} nodes list of node objects - * @param {Number} indent needed indent - * @param {Boolean} excludeCommas skip comma on start of line - */ - function checkNodesIndent(node, indent, excludeCommas) { - var nodeIndent = getNodeIndent(node, false, excludeCommas); - if (nodeIndent !== indent && isNodeFirstInLine(node)) { - report(node, indent, nodeIndent); + /** + * Checks node is the first in its own start line. By default it looks by start line. + * @param {ASTNode} node The node to check + * @return {Boolean} true if its the first in the its start line + */ + function isNodeFirstInLine(node) { + var token = node; + do { + token = sourceCode.getTokenBefore(token); + } while (token.type === 'JSXText'); + var startLine = node.loc.start.line; + var endLine = token ? token.loc.end.line : -1; + + return startLine !== endLine; } - } - return { - JSXOpeningElement: function(node) { - if (!node.parent || !node.parent.parent) { - return; + /** + * Check indent for nodes list + * @param {ASTNode[]} nodes list of node objects + * @param {Number} indent needed indent + * @param {Boolean} excludeCommas skip comma on start of line + */ + function checkNodesIndent(node, indent, excludeCommas) { + var nodeIndent = getNodeIndent(node, false, excludeCommas); + if (nodeIndent !== indent && isNodeFirstInLine(node)) { + report(node, indent, nodeIndent); } - var parentElementIndent = getNodeIndent(node.parent.parent); - var indent = node.parent.parent.loc.start.line === node.loc.start.line ? 0 : indentSize; - checkNodesIndent(node, parentElementIndent + indent); - }, - JSXClosingElement: function(node) { - if (!node.parent) { - return; - } - var peerElementIndent = getNodeIndent(node.parent.openingElement); - checkNodesIndent(node, peerElementIndent); } - }; -}; + return { + JSXOpeningElement: function(node) { + if (!node.parent || !node.parent.parent) { + return; + } + var parentElementIndent = getNodeIndent(node.parent.parent); + var indent = node.parent.parent.loc.start.line === node.loc.start.line ? 0 : indentSize; + checkNodesIndent(node, parentElementIndent + indent); + }, + JSXClosingElement: function(node) { + if (!node.parent) { + return; + } + var peerElementIndent = getNodeIndent(node.parent.openingElement); + checkNodesIndent(node, peerElementIndent); + } + }; -module.exports.schema = [{ - oneOf: [{ - enum: ['tab'] - }, { - type: 'integer' - }] -}]; + } +}; diff --git a/lib/rules/jsx-key.js b/lib/rules/jsx-key.js index 6601c29584..0425277313 100644 --- a/lib/rules/jsx-key.js +++ b/lib/rules/jsx-key.js @@ -12,65 +12,70 @@ var hasProp = require('jsx-ast-utils/hasProp'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - function checkIteratorElement(node) { - if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) { - context.report({ - node: node, - message: 'Missing "key" prop for element in iterator' - }); - } - } - - function getReturnStatement(body) { - return body.filter(function(item) { - return item.type === 'ReturnStatement'; - })[0]; - } - - return { - JSXElement: function(node) { - if (hasProp(node.openingElement.attributes, 'key')) { - return; - } + create: function(context) { - if (node.parent.type === 'ArrayExpression') { + function checkIteratorElement(node) { + if (node.type === 'JSXElement' && !hasProp(node.openingElement.attributes, 'key')) { context.report({ node: node, - message: 'Missing "key" prop for element in array' + message: 'Missing "key" prop for element in iterator' }); } - }, + } - // Array.prototype.map - CallExpression: function (node) { - if (node.callee && node.callee.type !== 'MemberExpression') { - return; - } + function getReturnStatement(body) { + return body.filter(function(item) { + return item.type === 'ReturnStatement'; + })[0]; + } - if (node.callee && node.callee.property && node.callee.property.name !== 'map') { - return; - } + return { + JSXElement: function(node) { + if (hasProp(node.openingElement.attributes, 'key')) { + return; + } - var fn = node.arguments[0]; - var isFn = fn && fn.type === 'FunctionExpression'; - var isArrFn = fn && fn.type === 'ArrowFunctionExpression'; + if (node.parent.type === 'ArrayExpression') { + context.report({ + node: node, + message: 'Missing "key" prop for element in array' + }); + } + }, - if (isArrFn && fn.body.type === 'JSXElement') { - checkIteratorElement(fn.body); - } + // Array.prototype.map + CallExpression: function (node) { + if (node.callee && node.callee.type !== 'MemberExpression') { + return; + } + + if (node.callee && node.callee.property && node.callee.property.name !== 'map') { + return; + } + + var fn = node.arguments[0]; + var isFn = fn && fn.type === 'FunctionExpression'; + var isArrFn = fn && fn.type === 'ArrowFunctionExpression'; - if (isFn || isArrFn) { - if (fn.body.type === 'BlockStatement') { - var returnStatement = getReturnStatement(fn.body.body); - if (returnStatement && returnStatement.argument) { - checkIteratorElement(returnStatement.argument); + if (isArrFn && fn.body.type === 'JSXElement') { + checkIteratorElement(fn.body); + } + + if (isFn || isArrFn) { + if (fn.body.type === 'BlockStatement') { + var returnStatement = getReturnStatement(fn.body.body); + if (returnStatement && returnStatement.argument) { + checkIteratorElement(returnStatement.argument); + } } } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/jsx-max-props-per-line.js b/lib/rules/jsx-max-props-per-line.js index 64621acb43..76dda68a7a 100644 --- a/lib/rules/jsx-max-props-per-line.js +++ b/lib/rules/jsx-max-props-per-line.js @@ -9,55 +9,61 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function (context) { +module.exports = { + meta: { + docs: {}, - var sourceCode = context.getSourceCode(); - var configuration = context.options[0] || {}; - var maximum = configuration.maximum || 1; + schema: [{ + type: 'object', + properties: { + maximum: { + type: 'integer', + minimum: 1 + } + } + }] + }, + + create: function (context) { + + var sourceCode = context.getSourceCode(); + var configuration = context.options[0] || {}; + var maximum = configuration.maximum || 1; - function getPropName(propNode) { - if (propNode.type === 'JSXSpreadAttribute') { - return sourceCode.getText(propNode.argument); + function getPropName(propNode) { + if (propNode.type === 'JSXSpreadAttribute') { + return sourceCode.getText(propNode.argument); + } + return propNode.name.name; } - return propNode.name.name; - } - return { - JSXOpeningElement: function (node) { - var props = {}; + return { + JSXOpeningElement: function (node) { + var props = {}; - node.attributes.forEach(function(decl) { - var line = decl.loc.start.line; - if (props[line]) { - props[line].push(decl); - } else { - props[line] = [decl]; - } - }); + node.attributes.forEach(function(decl) { + var line = decl.loc.start.line; + if (props[line]) { + props[line].push(decl); + } else { + props[line] = [decl]; + } + }); - for (var line in props) { - if (!props.hasOwnProperty(line)) { - continue; - } - if (props[line].length > maximum) { - var name = getPropName(props[line][maximum]); - context.report({ - node: props[line][maximum], - message: 'Prop `' + name + '` must be placed on a new line' - }); - break; + for (var line in props) { + if (!props.hasOwnProperty(line)) { + continue; + } + if (props[line].length > maximum) { + var name = getPropName(props[line][maximum]); + context.report({ + node: props[line][maximum], + message: 'Prop `' + name + '` must be placed on a new line' + }); + break; + } } } - } - }; -}; - -module.exports.schema = [{ - type: 'object', - properties: { - maximum: { - type: 'integer', - minimum: 1 - } + }; } -}]; +}; diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js index 2d63df3163..2561fe12e3 100644 --- a/lib/rules/jsx-no-bind.js +++ b/lib/rules/jsx-no-bind.js @@ -12,88 +12,94 @@ var propName = require('jsx-ast-utils/propName'); // Rule Definition // ----------------------------------------------------------------------------- -module.exports = Components.detect(function(context, components, utils) { - var configuration = context.options[0] || {}; +module.exports = { + meta: { + docs: {}, - return { - CallExpression: function(node) { - var callee = node.callee; - if ( - !configuration.allowBind && - (callee.type !== 'MemberExpression' || callee.property.name !== 'bind') - ) { - return; - } - var ancestors = context.getAncestors(callee).reverse(); - for (var i = 0, j = ancestors.length; i < j; i++) { + schema: [{ + type: 'object', + properties: { + allowArrowFunctions: { + default: false, + type: 'boolean' + }, + allowBind: { + default: false, + type: 'boolean' + }, + ignoreRefs: { + default: false, + type: 'boolean' + } + }, + additionalProperties: false + }] + }, + + create: Components.detect(function(context, components, utils) { + var configuration = context.options[0] || {}; + + return { + CallExpression: function(node) { + var callee = node.callee; if ( !configuration.allowBind && - (ancestors[i].type === 'MethodDefinition' && ancestors[i].key.name === 'render') || - (ancestors[i].type === 'Property' && ancestors[i].key.name === 'render') + (callee.type !== 'MemberExpression' || callee.property.name !== 'bind') ) { - if (utils.isReturningJSX(ancestors[i])) { - context.report({ - node: callee, - message: 'JSX props should not use .bind()' - }); + return; + } + var ancestors = context.getAncestors(callee).reverse(); + for (var i = 0, j = ancestors.length; i < j; i++) { + if ( + !configuration.allowBind && + (ancestors[i].type === 'MethodDefinition' && ancestors[i].key.name === 'render') || + (ancestors[i].type === 'Property' && ancestors[i].key.name === 'render') + ) { + if (utils.isReturningJSX(ancestors[i])) { + context.report({ + node: callee, + message: 'JSX props should not use .bind()' + }); + } + break; } - break; } - } - }, + }, - JSXAttribute: function(node) { - var isRef = configuration.ignoreRefs && propName(node) === 'ref'; - if (isRef || !node.value || !node.value.expression) { - return; - } - var valueNode = node.value.expression; - if ( - !configuration.allowBind && - valueNode.type === 'CallExpression' && - valueNode.callee.type === 'MemberExpression' && - valueNode.callee.property.name === 'bind' - ) { - context.report({ - node: node, - message: 'JSX props should not use .bind()' - }); - } else if ( - !configuration.allowArrowFunctions && - valueNode.type === 'ArrowFunctionExpression' - ) { - context.report({ - node: node, - message: 'JSX props should not use arrow functions' - }); - } else if ( - !configuration.allowBind && - valueNode.type === 'BindExpression' - ) { - context.report({ - node: node, - message: 'JSX props should not use ::' - }); + JSXAttribute: function(node) { + var isRef = configuration.ignoreRefs && propName(node) === 'ref'; + if (isRef || !node.value || !node.value.expression) { + return; + } + var valueNode = node.value.expression; + if ( + !configuration.allowBind && + valueNode.type === 'CallExpression' && + valueNode.callee.type === 'MemberExpression' && + valueNode.callee.property.name === 'bind' + ) { + context.report({ + node: node, + message: 'JSX props should not use .bind()' + }); + } else if ( + !configuration.allowArrowFunctions && + valueNode.type === 'ArrowFunctionExpression' + ) { + context.report({ + node: node, + message: 'JSX props should not use arrow functions' + }); + } else if ( + !configuration.allowBind && + valueNode.type === 'BindExpression' + ) { + context.report({ + node: node, + message: 'JSX props should not use ::' + }); + } } - } - }; -}); - -module.exports.schema = [{ - type: 'object', - properties: { - allowArrowFunctions: { - default: false, - type: 'boolean' - }, - allowBind: { - default: false, - type: 'boolean' - }, - ignoreRefs: { - default: false, - type: 'boolean' - } - }, - additionalProperties: false -}]; + }; + }) +}; diff --git a/lib/rules/jsx-no-comment-textnodes.js b/lib/rules/jsx-no-comment-textnodes.js index 7c3cf71d39..ccfda62cc3 100644 --- a/lib/rules/jsx-no-comment-textnodes.js +++ b/lib/rules/jsx-no-comment-textnodes.js @@ -8,31 +8,37 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - function reportLiteralNode(node) { - context.report(node, 'Comments inside children section of tag should be placed inside braces'); - } +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: {}, + additionalProperties: false + }] + }, + + create: function(context) { + function reportLiteralNode(node) { + context.report(node, 'Comments inside children section of tag should be placed inside braces'); + } - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - return { - Literal: function(node) { - if (/^\s*\/(\/|\*)/m.test(node.value)) { - // inside component, e.g.
literal
- if (node.parent.type !== 'JSXAttribute' && - node.parent.type !== 'JSXExpressionContainer' && - node.parent.type.indexOf('JSX') !== -1) { - reportLiteralNode(node); + return { + Literal: function(node) { + if (/^\s*\/(\/|\*)/m.test(node.value)) { + // inside component, e.g.
literal
+ if (node.parent.type !== 'JSXAttribute' && + node.parent.type !== 'JSXExpressionContainer' && + node.parent.type.indexOf('JSX') !== -1) { + reportLiteralNode(node); + } } } - } - }; + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: {}, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-no-duplicate-props.js b/lib/rules/jsx-no-duplicate-props.js index 75dd97bc0e..01b28e06de 100644 --- a/lib/rules/jsx-no-duplicate-props.js +++ b/lib/rules/jsx-no-duplicate-props.js @@ -9,45 +9,51 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function (context) { - - var configuration = context.options[0] || {}; - var ignoreCase = configuration.ignoreCase || false; - - return { - JSXOpeningElement: function (node) { - var props = {}; - - node.attributes.forEach(function(decl) { - if (decl.type === 'JSXSpreadAttribute') { - return; - } - - var name = decl.name.name; - - if (ignoreCase) { - name = name.toLowerCase(); +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: { + ignoreCase: { + type: 'boolean' } + }, + additionalProperties: false + }] + }, - if (props.hasOwnProperty(name)) { - context.report({ - node: decl, - message: 'No duplicate props allowed' - }); - } else { - props[name] = 1; - } - }); - } - }; + create: function (context) { + + var configuration = context.options[0] || {}; + var ignoreCase = configuration.ignoreCase || false; + + return { + JSXOpeningElement: function (node) { + var props = {}; + + node.attributes.forEach(function(decl) { + if (decl.type === 'JSXSpreadAttribute') { + return; + } + + var name = decl.name.name; + + if (ignoreCase) { + name = name.toLowerCase(); + } + + if (props.hasOwnProperty(name)) { + context.report({ + node: decl, + message: 'No duplicate props allowed' + }); + } else { + props[name] = 1; + } + }); + } + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - ignoreCase: { - type: 'boolean' - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-no-literals.js b/lib/rules/jsx-no-literals.js index e26f6b0f9d..a064a2c471 100644 --- a/lib/rules/jsx-no-literals.js +++ b/lib/rules/jsx-no-literals.js @@ -8,39 +8,45 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - - function reportLiteralNode(node) { - context.report({ - node: node, - message: 'Missing JSX expression container around literal string' - }); - } +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: {}, + additionalProperties: false + }] + }, + + create: function(context) { + + function reportLiteralNode(node) { + context.report({ + node: node, + message: 'Missing JSX expression container around literal string' + }); + } - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- - - return { - - Literal: function(node) { - if ( - !/^[\s]+$/.test(node.value) && - node.parent && - node.parent.type !== 'JSXExpressionContainer' && - node.parent.type !== 'JSXAttribute' && - node.parent.type.indexOf('JSX') !== -1 - ) { - reportLiteralNode(node); + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + + Literal: function(node) { + if ( + !/^[\s]+$/.test(node.value) && + node.parent && + node.parent.type !== 'JSXExpressionContainer' && + node.parent.type !== 'JSXAttribute' && + node.parent.type.indexOf('JSX') !== -1 + ) { + reportLiteralNode(node); + } } - } - }; + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: {}, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-no-target-blank.js b/lib/rules/jsx-no-target-blank.js index 6aec5d32b0..1fe4023d71 100644 --- a/lib/rules/jsx-no-target-blank.js +++ b/lib/rules/jsx-no-target-blank.js @@ -8,28 +8,33 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - return { - JSXAttribute: function(node) { - if (node.name.name === 'target' && node.value.value === '_blank') { - var relFound = false; - var attrs = node.parent.attributes; - for (var idx in attrs) { - if (attrs[idx].name && attrs[idx].name.name === 'rel') { - var tags = attrs[idx].value.value.split(' '); - if (tags.indexOf('noopener') >= 0 && tags.indexOf('noreferrer') >= 0) { - relFound = true; - break; +module.exports = { + meta: { + docs: {}, + schema: [] + }, + + create: function(context) { + return { + JSXAttribute: function(node) { + if (node.name.name === 'target' && node.value.value === '_blank') { + var relFound = false; + var attrs = node.parent.attributes; + for (var idx in attrs) { + if (attrs[idx].name && attrs[idx].name.name === 'rel') { + var tags = attrs[idx].value.value.split(' '); + if (tags.indexOf('noopener') >= 0 && tags.indexOf('noreferrer') >= 0) { + relFound = true; + break; + } } } - } - if (!relFound) { - context.report(node, 'Using target="_blank" without rel="noopener noreferrer" ' + - 'is a security risk: see https://mathiasbynens.github.io/rel-noopener'); + if (!relFound) { + context.report(node, 'Using target="_blank" without rel="noopener noreferrer" ' + + 'is a security risk: see https://mathiasbynens.github.io/rel-noopener'); + } } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/jsx-no-undef.js b/lib/rules/jsx-no-undef.js index 7e69c0df97..dd6b7e4659 100644 --- a/lib/rules/jsx-no-undef.js +++ b/lib/rules/jsx-no-undef.js @@ -19,73 +19,78 @@ function isTagName(name) { // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - /** - * Compare an identifier with the variables declared in the scope - * @param {ASTNode} node - Identifier or JSXIdentifier node - * @returns {void} - */ - function checkIdentifierInJSX(node) { - var scope = context.getScope(); - var variables = scope.variables; - var i; - var len; + create: function(context) { - // Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX) - if (node.name === 'this') { - return; - } + /** + * Compare an identifier with the variables declared in the scope + * @param {ASTNode} node - Identifier or JSXIdentifier node + * @returns {void} + */ + function checkIdentifierInJSX(node) { + var scope = context.getScope(); + var variables = scope.variables; + var i; + var len; - while (scope.type !== 'global') { - scope = scope.upper; - variables = scope.variables.concat(variables); - } - if (scope.childScopes.length) { - variables = scope.childScopes[0].variables.concat(variables); - // Temporary fix for babel-eslint - if (scope.childScopes[0].childScopes.length) { - variables = scope.childScopes[0].childScopes[0].variables.concat(variables); - } - } - - for (i = 0, len = variables.length; i < len; i++) { - if (variables[i].name === node.name) { + // Ignore 'this' keyword (also maked as JSXIdentifier when used in JSX) + if (node.name === 'this') { return; } - } - context.report({ - node: node, - message: '\'' + node.name + '\' is not defined.' - }); - } + while (scope.type !== 'global') { + scope = scope.upper; + variables = scope.variables.concat(variables); + } + if (scope.childScopes.length) { + variables = scope.childScopes[0].variables.concat(variables); + // Temporary fix for babel-eslint + if (scope.childScopes[0].childScopes.length) { + variables = scope.childScopes[0].childScopes[0].variables.concat(variables); + } + } - return { - JSXOpeningElement: function(node) { - switch (node.name.type) { - case 'JSXIdentifier': - node = node.name; - if (isTagName(node.name)) { - return; - } - break; - case 'JSXMemberExpression': - node = node.name; - do { - node = node.object; - } while (node && node.type !== 'JSXIdentifier'); - break; - case 'JSXNamespacedName': - node = node.name.namespace; - break; - default: - break; + for (i = 0, len = variables.length; i < len; i++) { + if (variables[i].name === node.name) { + return; + } } - checkIdentifierInJSX(node); + + context.report({ + node: node, + message: '\'' + node.name + '\' is not defined.' + }); } - }; -}; + return { + JSXOpeningElement: function(node) { + switch (node.name.type) { + case 'JSXIdentifier': + node = node.name; + if (isTagName(node.name)) { + return; + } + break; + case 'JSXMemberExpression': + node = node.name; + do { + node = node.object; + } while (node && node.type !== 'JSXIdentifier'); + break; + case 'JSXNamespacedName': + node = node.name.namespace; + break; + default: + break; + } + checkIdentifierInJSX(node); + } + }; -module.exports.schema = []; + } +}; diff --git a/lib/rules/jsx-pascal-case.js b/lib/rules/jsx-pascal-case.js index b7277fb9f1..05bfb2eb7c 100644 --- a/lib/rules/jsx-pascal-case.js +++ b/lib/rules/jsx-pascal-case.js @@ -19,48 +19,54 @@ var ALL_CAPS_TAG_REGEX = /^[A-Z0-9]+$/; // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, - var configuration = context.options[0] || {}; - var allowAllCaps = configuration.allowAllCaps || false; - var ignore = configuration.ignore || []; + schema: [{ + type: 'object', + properties: { + allowAllCaps: { + type: 'boolean' + }, + ignore: { + type: 'array' + } + }, + additionalProperties: false + }] + }, - return { - JSXOpeningElement: function(node) { - var name = elementType(node); + create: function(context) { - // Get namespace if the type is JSXNamespacedName or JSXMemberExpression - if (name.indexOf(':') > -1) { - name = name.substring(0, name.indexOf(':')); - } else if (name.indexOf('.') > -1) { - name = name.substring(0, name.indexOf('.')); - } + var configuration = context.options[0] || {}; + var allowAllCaps = configuration.allowAllCaps || false; + var ignore = configuration.ignore || []; + + return { + JSXOpeningElement: function(node) { + var name = elementType(node); - var isPascalCase = PASCAL_CASE_REGEX.test(name); - var isCompatTag = COMPAT_TAG_REGEX.test(name); - var isAllowedAllCaps = allowAllCaps && ALL_CAPS_TAG_REGEX.test(name); - var isIgnored = ignore.indexOf(name) !== -1; + // Get namespace if the type is JSXNamespacedName or JSXMemberExpression + if (name.indexOf(':') > -1) { + name = name.substring(0, name.indexOf(':')); + } else if (name.indexOf('.') > -1) { + name = name.substring(0, name.indexOf('.')); + } - if (!isPascalCase && !isCompatTag && !isAllowedAllCaps && !isIgnored) { - context.report({ - node: node, - message: 'Imported JSX component ' + name + ' must be in PascalCase' - }); + var isPascalCase = PASCAL_CASE_REGEX.test(name); + var isCompatTag = COMPAT_TAG_REGEX.test(name); + var isAllowedAllCaps = allowAllCaps && ALL_CAPS_TAG_REGEX.test(name); + var isIgnored = ignore.indexOf(name) !== -1; + + if (!isPascalCase && !isCompatTag && !isAllowedAllCaps && !isIgnored) { + context.report({ + node: node, + message: 'Imported JSX component ' + name + ' must be in PascalCase' + }); + } } - } - }; + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - allowAllCaps: { - type: 'boolean' - }, - ignore: { - type: 'array' - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-require-extension.js b/lib/rules/jsx-require-extension.js index dde310db63..acecfe4ffe 100644 --- a/lib/rules/jsx-require-extension.js +++ b/lib/rules/jsx-require-extension.js @@ -20,69 +20,75 @@ var PKG_REGEX = /^[^\.]((?!\/).)*$/; // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: { + extensions: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + }] + }, - function isPackage(id) { - return PKG_REGEX.test(id); - } + create: function(context) { - function isRequire(expression) { - return expression.callee.name === 'require'; - } + function isPackage(id) { + return PKG_REGEX.test(id); + } - function getId(expression) { - return expression.arguments[0] && expression.arguments[0].value; - } + function isRequire(expression) { + return expression.callee.name === 'require'; + } - function getExtension(id) { - return path.extname(id || ''); - } + function getId(expression) { + return expression.arguments[0] && expression.arguments[0].value; + } - function getExtensionsConfig() { - return context.options[0] && context.options[0].extensions || DEFAULTS.extensions; - } + function getExtension(id) { + return path.extname(id || ''); + } - var forbiddenExtensions = getExtensionsConfig().reduce(function (extensions, extension) { - extensions[extension] = true; - return extensions; - }, Object.create(null)); + function getExtensionsConfig() { + return context.options[0] && context.options[0].extensions || DEFAULTS.extensions; + } - function isForbiddenExtension(ext) { - return ext in forbiddenExtensions; - } + var forbiddenExtensions = getExtensionsConfig().reduce(function (extensions, extension) { + extensions[extension] = true; + return extensions; + }, Object.create(null)); - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- - - return { - - CallExpression: function(node) { - if (isRequire(node)) { - var id = getId(node); - var ext = getExtension(id); - if (!isPackage(id) && isForbiddenExtension(ext)) { - context.report({ - node: node, - message: 'Unable to require module with extension \'' + ext + '\'' - }); + function isForbiddenExtension(ext) { + return ext in forbiddenExtensions; + } + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + + CallExpression: function(node) { + if (isRequire(node)) { + var id = getId(node); + var ext = getExtension(id); + if (!isPackage(id) && isForbiddenExtension(ext)) { + context.report({ + node: node, + message: 'Unable to require module with extension \'' + ext + '\'' + }); + } } } - } - }; + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - extensions: { - type: 'array', - items: { - type: 'string' - } - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-sort-props.js b/lib/rules/jsx-sort-props.js index a448e2eb09..26e968bc7e 100644 --- a/lib/rules/jsx-sort-props.js +++ b/lib/rules/jsx-sort-props.js @@ -14,107 +14,113 @@ function isCallbackPropName(name) { return /^on[A-Z]/.test(name); } -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, - var configuration = context.options[0] || {}; - var ignoreCase = configuration.ignoreCase || false; - var callbacksLast = configuration.callbacksLast || false; - var shorthandFirst = configuration.shorthandFirst || false; - var shorthandLast = configuration.shorthandLast || false; - - return { - JSXOpeningElement: function(node) { - node.attributes.reduce(function(memo, decl, idx, attrs) { - if (decl.type === 'JSXSpreadAttribute') { - return attrs[idx + 1]; + schema: [{ + type: 'object', + properties: { + // Whether callbacks (prefixed with "on") should be listed at the very end, + // after all other props. Supersedes shorthandLast. + callbacksLast: { + type: 'boolean' + }, + // Whether shorthand properties (without a value) should be listed first + shorthandFirst: { + type: 'boolean' + }, + // Whether shorthand properties (without a value) should be listed last + shorthandLast: { + type: 'boolean' + }, + ignoreCase: { + type: 'boolean' } + }, + additionalProperties: false + }] + }, - var previousPropName = propName(memo); - var currentPropName = propName(decl); - var previousValue = memo.value; - var currentValue = decl.value; - var previousIsCallback = isCallbackPropName(previousPropName); - var currentIsCallback = isCallbackPropName(currentPropName); + create: function(context) { - if (ignoreCase) { - previousPropName = previousPropName.toLowerCase(); - currentPropName = currentPropName.toLowerCase(); - } + var configuration = context.options[0] || {}; + var ignoreCase = configuration.ignoreCase || false; + var callbacksLast = configuration.callbacksLast || false; + var shorthandFirst = configuration.shorthandFirst || false; + var shorthandLast = configuration.shorthandLast || false; - if (callbacksLast) { - if (!previousIsCallback && currentIsCallback) { - // Entering the callback prop section - return decl; + return { + JSXOpeningElement: function(node) { + node.attributes.reduce(function(memo, decl, idx, attrs) { + if (decl.type === 'JSXSpreadAttribute') { + return attrs[idx + 1]; } - if (previousIsCallback && !currentIsCallback) { - // Encountered a non-callback prop after a callback prop - context.report({ - node: memo, - message: 'Callbacks must be listed after all other props' - }); - return memo; + + var previousPropName = propName(memo); + var currentPropName = propName(decl); + var previousValue = memo.value; + var currentValue = decl.value; + var previousIsCallback = isCallbackPropName(previousPropName); + var currentIsCallback = isCallbackPropName(currentPropName); + + if (ignoreCase) { + previousPropName = previousPropName.toLowerCase(); + currentPropName = currentPropName.toLowerCase(); } - } - if (shorthandFirst) { - if (currentValue && !previousValue) { - return decl; + if (callbacksLast) { + if (!previousIsCallback && currentIsCallback) { + // Entering the callback prop section + return decl; + } + if (previousIsCallback && !currentIsCallback) { + // Encountered a non-callback prop after a callback prop + context.report({ + node: memo, + message: 'Callbacks must be listed after all other props' + }); + return memo; + } } - if (!currentValue && previousValue) { - context.report({ - node: memo, - message: 'Shorthand props must be listed before all other props' - }); - return memo; + + if (shorthandFirst) { + if (currentValue && !previousValue) { + return decl; + } + if (!currentValue && previousValue) { + context.report({ + node: memo, + message: 'Shorthand props must be listed before all other props' + }); + return memo; + } } - } - if (shorthandLast) { - if (!currentValue && previousValue) { - return decl; + if (shorthandLast) { + if (!currentValue && previousValue) { + return decl; + } + if (currentValue && !previousValue) { + context.report({ + node: memo, + message: 'Shorthand props must be listed after all other props' + }); + return memo; + } } - if (currentValue && !previousValue) { + + if (currentPropName < previousPropName) { context.report({ - node: memo, - message: 'Shorthand props must be listed after all other props' + node: decl, + message: 'Props should be sorted alphabetically' }); return memo; } - } - - if (currentPropName < previousPropName) { - context.report({ - node: decl, - message: 'Props should be sorted alphabetically' - }); - return memo; - } - return decl; - }, node.attributes[0]); - } - }; + return decl; + }, node.attributes[0]); + } + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - // Whether callbacks (prefixed with "on") should be listed at the very end, - // after all other props. Supersedes shorthandLast. - callbacksLast: { - type: 'boolean' - }, - // Whether shorthand properties (without a value) should be listed first - shorthandFirst: { - type: 'boolean' - }, - // Whether shorthand properties (without a value) should be listed last - shorthandLast: { - type: 'boolean' - }, - ignoreCase: { - type: 'boolean' - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/jsx-space-before-closing.js b/lib/rules/jsx-space-before-closing.js index 329cb6a5ea..e05b98aae5 100644 --- a/lib/rules/jsx-space-before-closing.js +++ b/lib/rules/jsx-space-before-closing.js @@ -8,67 +8,74 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + fixable: 'code', - var configuration = context.options[0] || 'always'; - var sourceCode = context.getSourceCode(); + schema: [{ + enum: ['always', 'never'] + }] + }, - var NEVER_MESSAGE = 'A space is forbidden before closing bracket'; - var ALWAYS_MESSAGE = 'A space is required before closing bracket'; + create: function(context) { - /** - * Find the token before the closing bracket. - * @param {ASTNode} node - The JSX element node. - * @returns {Token} The token before the closing bracket. - */ - function getTokenBeforeClosingBracket(node) { - var attributes = node.attributes; - if (attributes.length === 0) { - return node.name; - } - return attributes[ attributes.length - 1 ]; - } + var configuration = context.options[0] || 'always'; + var sourceCode = context.getSourceCode(); - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + var NEVER_MESSAGE = 'A space is forbidden before closing bracket'; + var ALWAYS_MESSAGE = 'A space is required before closing bracket'; - return { - JSXOpeningElement: function(node) { - if (!node.selfClosing) { - return; + /** + * Find the token before the closing bracket. + * @param {ASTNode} node - The JSX element node. + * @returns {Token} The token before the closing bracket. + */ + function getTokenBeforeClosingBracket(node) { + var attributes = node.attributes; + if (attributes.length === 0) { + return node.name; } + return attributes[ attributes.length - 1 ]; + } - var leftToken = getTokenBeforeClosingBracket(node); - var closingSlash = sourceCode.getTokenAfter(leftToken); + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - if (leftToken.loc.end.line !== closingSlash.loc.start.line) { - return; - } + return { + JSXOpeningElement: function(node) { + if (!node.selfClosing) { + return; + } + + var leftToken = getTokenBeforeClosingBracket(node); + var closingSlash = sourceCode.getTokenAfter(leftToken); - if (configuration === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { - context.report({ - loc: closingSlash.loc.start, - message: ALWAYS_MESSAGE, - fix: function(fixer) { - return fixer.insertTextBefore(closingSlash, ' '); - } - }); - } else if (configuration === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { - context.report({ - loc: closingSlash.loc.start, - message: NEVER_MESSAGE, - fix: function(fixer) { - var previousToken = sourceCode.getTokenBefore(closingSlash); - return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]); - } - }); + if (leftToken.loc.end.line !== closingSlash.loc.start.line) { + return; + } + + if (configuration === 'always' && !sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { + context.report({ + loc: closingSlash.loc.start, + message: ALWAYS_MESSAGE, + fix: function(fixer) { + return fixer.insertTextBefore(closingSlash, ' '); + } + }); + } else if (configuration === 'never' && sourceCode.isSpaceBetweenTokens(leftToken, closingSlash)) { + context.report({ + loc: closingSlash.loc.start, + message: NEVER_MESSAGE, + fix: function(fixer) { + var previousToken = sourceCode.getTokenBefore(closingSlash); + return fixer.removeRange([previousToken.range[1], closingSlash.range[0]]); + } + }); + } } - } - }; + }; + } }; - -module.exports.schema = [{ - enum: ['always', 'never'] -}]; diff --git a/lib/rules/jsx-uses-react.js b/lib/rules/jsx-uses-react.js index 5cac4b404e..f329d8334c 100644 --- a/lib/rules/jsx-uses-react.js +++ b/lib/rules/jsx-uses-react.js @@ -11,26 +11,31 @@ var pragmaUtil = require('../util/pragma'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - var pragma = pragmaUtil.getFromContext(context); + create: function(context) { - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + var pragma = pragmaUtil.getFromContext(context); - return { + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - JSXOpeningElement: function() { - variableUtil.markVariableAsUsed(context, pragma); - }, + return { - BlockComment: function(node) { - pragma = pragmaUtil.getFromNode(node) || pragma; - } + JSXOpeningElement: function() { + variableUtil.markVariableAsUsed(context, pragma); + }, - }; + BlockComment: function(node) { + pragma = pragmaUtil.getFromNode(node) || pragma; + } -}; + }; -module.exports.schema = []; + } +}; diff --git a/lib/rules/jsx-uses-vars.js b/lib/rules/jsx-uses-vars.js index 8717b17371..a4f461628a 100644 --- a/lib/rules/jsx-uses-vars.js +++ b/lib/rules/jsx-uses-vars.js @@ -10,36 +10,41 @@ var variableUtil = require('../util/variable'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - - return { - JSXExpressionContainer: function(node) { - if (node.expression.type !== 'Identifier') { - return; - } - variableUtil.markVariableAsUsed(context, node.expression.name); - }, - - JSXOpeningElement: function(node) { - var name; - if (node.name.namespace && node.name.namespace.name) { - // - name = node.name.namespace.name; - } else if (node.name.name) { - // - name = node.name.name; - } else if (node.name.object && node.name.object.name) { - // - node.name.object.name - name = node.name.object.name; - } else { - return; +module.exports = { + meta: { + docs: {}, + schema: [] + }, + + create: function(context) { + + return { + JSXExpressionContainer: function(node) { + if (node.expression.type !== 'Identifier') { + return; + } + variableUtil.markVariableAsUsed(context, node.expression.name); + }, + + JSXOpeningElement: function(node) { + var name; + if (node.name.namespace && node.name.namespace.name) { + // + name = node.name.namespace.name; + } else if (node.name.name) { + // + name = node.name.name; + } else if (node.name.object && node.name.object.name) { + // - node.name.object.name + name = node.name.object.name; + } else { + return; + } + + variableUtil.markVariableAsUsed(context, name); } - variableUtil.markVariableAsUsed(context, name); - } - - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/jsx-wrap-multilines.js b/lib/rules/jsx-wrap-multilines.js index a28b9a7341..bca54662dd 100644 --- a/lib/rules/jsx-wrap-multilines.js +++ b/lib/rules/jsx-wrap-multilines.js @@ -18,86 +18,93 @@ var DEFAULTS = { // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + fixable: 'code', + + schema: [{ + type: 'object', + properties: { + declaration: { + type: 'boolean' + }, + assignment: { + type: 'boolean' + }, + return: { + type: 'boolean' + } + }, + additionalProperties: false + }] + }, - var sourceCode = context.getSourceCode(); + create: function(context) { - function isParenthesised(node) { - var previousToken = sourceCode.getTokenBefore(node); - var nextToken = sourceCode.getTokenAfter(node); + var sourceCode = context.getSourceCode(); - return previousToken && nextToken && - previousToken.value === '(' && previousToken.range[1] <= node.range[0] && - nextToken.value === ')' && nextToken.range[0] >= node.range[1]; - } + function isParenthesised(node) { + var previousToken = sourceCode.getTokenBefore(node); + var nextToken = sourceCode.getTokenAfter(node); - function isMultilines(node) { - return node.loc.start.line !== node.loc.end.line; - } + return previousToken && nextToken && + previousToken.value === '(' && previousToken.range[1] <= node.range[0] && + nextToken.value === ')' && nextToken.range[0] >= node.range[1]; + } - function check(node) { - if (!node || node.type !== 'JSXElement') { - return; + function isMultilines(node) { + return node.loc.start.line !== node.loc.end.line; } - if (!isParenthesised(node) && isMultilines(node)) { - context.report({ - node: node, - message: 'Missing parentheses around multilines JSX', - fix: function(fixer) { - return fixer.replaceText(node, '(' + sourceCode.getText(node) + ')'); - } - }); + function check(node) { + if (!node || node.type !== 'JSXElement') { + return; + } + + if (!isParenthesised(node) && isMultilines(node)) { + context.report({ + node: node, + message: 'Missing parentheses around multilines JSX', + fix: function(fixer) { + return fixer.replaceText(node, '(' + sourceCode.getText(node) + ')'); + } + }); + } } - } - function isEnabled(type) { - var userOptions = context.options[0] || {}; - if (({}).hasOwnProperty.call(userOptions, type)) { - return userOptions[type]; + function isEnabled(type) { + var userOptions = context.options[0] || {}; + if (({}).hasOwnProperty.call(userOptions, type)) { + return userOptions[type]; + } + return DEFAULTS[type]; } - return DEFAULTS[type]; - } - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - return { + return { - VariableDeclarator: function(node) { - if (isEnabled('declaration')) { - check(node.init); - } - }, + VariableDeclarator: function(node) { + if (isEnabled('declaration')) { + check(node.init); + } + }, - AssignmentExpression: function(node) { - if (isEnabled('assignment')) { - check(node.right); - } - }, + AssignmentExpression: function(node) { + if (isEnabled('assignment')) { + check(node.right); + } + }, - ReturnStatement: function(node) { - if (isEnabled('return')) { - check(node.argument); + ReturnStatement: function(node) { + if (isEnabled('return')) { + check(node.argument); + } } - } - }; + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - declaration: { - type: 'boolean' - }, - assignment: { - type: 'boolean' - }, - return: { - type: 'boolean' - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/no-comment-textnodes.js b/lib/rules/no-comment-textnodes.js index 954999e0e1..eea5a09782 100644 --- a/lib/rules/no-comment-textnodes.js +++ b/lib/rules/no-comment-textnodes.js @@ -13,24 +13,30 @@ var util = require('util'); var jsxNoCommentTextnodes = require('./jsx-no-comment-textnodes'); var isWarnedForDeprecation = false; -module.exports = function(context) { - return util._extend(jsxNoCommentTextnodes(context), { - Program: function() { - if (isWarnedForDeprecation || /\=-(f|-format)=/.test(process.argv.join('='))) { - return; - } +module.exports = { + meta: { + docs: {}, - /* eslint-disable no-console */ - console.log('The react/no-comment-textnodes rule is deprecated. Please ' + - 'use the react/jsx-no-comment-textnodes rule instead.'); - /* eslint-enable no-console */ - isWarnedForDeprecation = true; - } - }); -}; + schema: [{ + type: 'object', + properties: {}, + additionalProperties: false + }] + }, + + create: function(context) { + return util._extend(jsxNoCommentTextnodes(context), { + Program: function() { + if (isWarnedForDeprecation || /\=-(f|-format)=/.test(process.argv.join('='))) { + return; + } -module.exports.schema = [{ - type: 'object', - properties: {}, - additionalProperties: false -}]; + /* eslint-disable no-console */ + console.log('The react/no-comment-textnodes rule is deprecated. Please ' + + 'use the react/jsx-no-comment-textnodes rule instead.'); + /* eslint-enable no-console */ + isWarnedForDeprecation = true; + } + }); + } +}; diff --git a/lib/rules/no-danger.js b/lib/rules/no-danger.js index 83ac55475a..3dcbf474f1 100644 --- a/lib/rules/no-danger.js +++ b/lib/rules/no-danger.js @@ -46,24 +46,29 @@ function isDangerous(name) { // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - return { + create: function(context) { - JSXAttribute: function(node) { - if (isTagName(node.parent.name.name) && isDangerous(node.name.name)) { - context.report({ - node: node, - message: DANGEROUS_MESSAGE, - data: { - name: node.name.name - } - }); + return { + + JSXAttribute: function(node) { + if (isTagName(node.parent.name.name) && isDangerous(node.name.name)) { + context.report({ + node: node, + message: DANGEROUS_MESSAGE, + data: { + name: node.name.name + } + }); + } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js index d01bbec5db..24b8b70c11 100644 --- a/lib/rules/no-deprecated.js +++ b/lib/rules/no-deprecated.js @@ -18,85 +18,90 @@ var DEPRECATED_MESSAGE = '{{oldMethod}} is deprecated since React {{version}}{{n // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - - var sourceCode = context.getSourceCode(); - var pragma = pragmaUtil.getFromContext(context); - - function getDeprecated() { - var deprecated = { - MemberExpression: {} - }; - // 0.12.0 - deprecated.MemberExpression[pragma + '.renderComponent'] = ['0.12.0', pragma + '.render']; - deprecated.MemberExpression[pragma + '.renderComponentToString'] = ['0.12.0', pragma + '.renderToString']; - deprecated.MemberExpression[pragma + '.renderComponentToStaticMarkup'] = [ - '0.12.0', - pragma + '.renderToStaticMarkup' - ]; - deprecated.MemberExpression[pragma + '.isValidComponent'] = ['0.12.0', pragma + '.isValidElement']; - deprecated.MemberExpression[pragma + '.PropTypes.component'] = ['0.12.0', pragma + '.PropTypes.element']; - deprecated.MemberExpression[pragma + '.PropTypes.renderable'] = ['0.12.0', pragma + '.PropTypes.node']; - deprecated.MemberExpression[pragma + '.isValidClass'] = ['0.12.0']; - deprecated.MemberExpression['this.transferPropsTo'] = ['0.12.0', 'spread operator ({...})']; - // 0.13.0 - deprecated.MemberExpression[pragma + '.addons.classSet'] = ['0.13.0', 'the npm module classnames']; - deprecated.MemberExpression[pragma + '.addons.cloneWithProps'] = ['0.13.0', pragma + '.cloneElement']; - // 0.14.0 - deprecated.MemberExpression[pragma + '.render'] = ['0.14.0', 'ReactDOM.render']; - deprecated.MemberExpression[pragma + '.unmountComponentAtNode'] = ['0.14.0', 'ReactDOM.unmountComponentAtNode']; - deprecated.MemberExpression[pragma + '.findDOMNode'] = ['0.14.0', 'ReactDOM.findDOMNode']; - deprecated.MemberExpression[pragma + '.renderToString'] = ['0.14.0', 'ReactDOMServer.renderToString']; - deprecated.MemberExpression[pragma + '.renderToStaticMarkup'] = ['0.14.0', 'ReactDOMServer.renderToStaticMarkup']; - // 15.0.0 - deprecated.MemberExpression[pragma + '.addons.LinkedStateMixin'] = ['15.0.0']; - deprecated.MemberExpression['ReactPerf.printDOM'] = ['15.0.0', 'ReactPerf.printOperations']; - deprecated.MemberExpression['Perf.printDOM'] = ['15.0.0', 'Perf.printOperations']; - deprecated.MemberExpression['ReactPerf.getMeasurementsSummaryMap'] = ['15.0.0', 'ReactPerf.getWasted']; - deprecated.MemberExpression['Perf.getMeasurementsSummaryMap'] = ['15.0.0', 'Perf.getWasted']; - - return deprecated; - } +module.exports = { + meta: { + docs: {}, + schema: [] + }, + + create: function(context) { + + var sourceCode = context.getSourceCode(); + var pragma = pragmaUtil.getFromContext(context); + + function getDeprecated() { + var deprecated = { + MemberExpression: {} + }; + // 0.12.0 + deprecated.MemberExpression[pragma + '.renderComponent'] = ['0.12.0', pragma + '.render']; + deprecated.MemberExpression[pragma + '.renderComponentToString'] = ['0.12.0', pragma + '.renderToString']; + deprecated.MemberExpression[pragma + '.renderComponentToStaticMarkup'] = [ + '0.12.0', + pragma + '.renderToStaticMarkup' + ]; + deprecated.MemberExpression[pragma + '.isValidComponent'] = ['0.12.0', pragma + '.isValidElement']; + deprecated.MemberExpression[pragma + '.PropTypes.component'] = ['0.12.0', pragma + '.PropTypes.element']; + deprecated.MemberExpression[pragma + '.PropTypes.renderable'] = ['0.12.0', pragma + '.PropTypes.node']; + deprecated.MemberExpression[pragma + '.isValidClass'] = ['0.12.0']; + deprecated.MemberExpression['this.transferPropsTo'] = ['0.12.0', 'spread operator ({...})']; + // 0.13.0 + deprecated.MemberExpression[pragma + '.addons.classSet'] = ['0.13.0', 'the npm module classnames']; + deprecated.MemberExpression[pragma + '.addons.cloneWithProps'] = ['0.13.0', pragma + '.cloneElement']; + // 0.14.0 + deprecated.MemberExpression[pragma + '.render'] = ['0.14.0', 'ReactDOM.render']; + deprecated.MemberExpression[pragma + '.unmountComponentAtNode'] = ['0.14.0', 'ReactDOM.unmountComponentAtNode']; + deprecated.MemberExpression[pragma + '.findDOMNode'] = ['0.14.0', 'ReactDOM.findDOMNode']; + deprecated.MemberExpression[pragma + '.renderToString'] = ['0.14.0', 'ReactDOMServer.renderToString']; + deprecated.MemberExpression[pragma + '.renderToStaticMarkup'] = ['0.14.0', 'ReactDOMServer.renderToStaticMarkup']; + // 15.0.0 + deprecated.MemberExpression[pragma + '.addons.LinkedStateMixin'] = ['15.0.0']; + deprecated.MemberExpression['ReactPerf.printDOM'] = ['15.0.0', 'ReactPerf.printOperations']; + deprecated.MemberExpression['Perf.printDOM'] = ['15.0.0', 'Perf.printOperations']; + deprecated.MemberExpression['ReactPerf.getMeasurementsSummaryMap'] = ['15.0.0', 'ReactPerf.getWasted']; + deprecated.MemberExpression['Perf.getMeasurementsSummaryMap'] = ['15.0.0', 'Perf.getWasted']; + + return deprecated; + } - function isDeprecated(type, method) { - var deprecated = getDeprecated(); + function isDeprecated(type, method) { + var deprecated = getDeprecated(); - return ( - deprecated[type] && - deprecated[type][method] && - versionUtil.test(context, deprecated[type][method][0]) - ); - } + return ( + deprecated[type] && + deprecated[type][method] && + versionUtil.test(context, deprecated[type][method][0]) + ); + } - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - return { + return { - MemberExpression: function(node) { - var method = sourceCode.getText(node); - if (!isDeprecated(node.type, method)) { - return; - } - var deprecated = getDeprecated(); - context.report({ - node: node, - message: DEPRECATED_MESSAGE, - data: { - oldMethod: method, - version: deprecated[node.type][method][0], - newMethod: deprecated[node.type][method][1] ? ', use ' + deprecated[node.type][method][1] + ' instead' : '' + MemberExpression: function(node) { + var method = sourceCode.getText(node); + if (!isDeprecated(node.type, method)) { + return; } - }); - }, - - BlockComment: function(node) { - pragma = pragmaUtil.getFromNode(node) || pragma; - } + var deprecated = getDeprecated(); + context.report({ + node: node, + message: DEPRECATED_MESSAGE, + data: { + oldMethod: method, + version: deprecated[node.type][method][0], + newMethod: deprecated[node.type][method][1] ? ', use ' + deprecated[node.type][method][1] + ' instead' : '' + } + }); + }, + + BlockComment: function(node) { + pragma = pragmaUtil.getFromNode(node) || pragma; + } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/no-did-mount-set-state.js b/lib/rules/no-did-mount-set-state.js index 157f38900a..a0b93bb522 100644 --- a/lib/rules/no-did-mount-set-state.js +++ b/lib/rules/no-did-mount-set-state.js @@ -8,49 +8,55 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, - var mode = context.options[0] || 'allow-in-func'; + schema: [{ + enum: ['disallow-in-func'] + }] + }, - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + create: function(context) { - return { + var mode = context.options[0] || 'allow-in-func'; - CallExpression: function(node) { - var callee = node.callee; - if ( - callee.type !== 'MemberExpression' || - callee.object.type !== 'ThisExpression' || - callee.property.name !== 'setState' - ) { - return; - } - var ancestors = context.getAncestors(callee).reverse(); - var depth = 0; - for (var i = 0, j = ancestors.length; i < j; i++) { - if (/Function(Expression|Declaration)$/.test(ancestors[i].type)) { - depth++; - } + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + + CallExpression: function(node) { + var callee = node.callee; if ( - (ancestors[i].type !== 'Property' && ancestors[i].type !== 'MethodDefinition') || - ancestors[i].key.name !== 'componentDidMount' || - (mode !== 'disallow-in-func' && depth > 1) + callee.type !== 'MemberExpression' || + callee.object.type !== 'ThisExpression' || + callee.property.name !== 'setState' ) { - continue; + return; + } + var ancestors = context.getAncestors(callee).reverse(); + var depth = 0; + for (var i = 0, j = ancestors.length; i < j; i++) { + if (/Function(Expression|Declaration)$/.test(ancestors[i].type)) { + depth++; + } + if ( + (ancestors[i].type !== 'Property' && ancestors[i].type !== 'MethodDefinition') || + ancestors[i].key.name !== 'componentDidMount' || + (mode !== 'disallow-in-func' && depth > 1) + ) { + continue; + } + context.report({ + node: callee, + message: 'Do not use setState in componentDidMount' + }); + break; } - context.report({ - node: callee, - message: 'Do not use setState in componentDidMount' - }); - break; } - } - }; + }; + } }; - -module.exports.schema = [{ - enum: ['disallow-in-func'] -}]; diff --git a/lib/rules/no-did-update-set-state.js b/lib/rules/no-did-update-set-state.js index 2a2937f92b..212b50e0ed 100644 --- a/lib/rules/no-did-update-set-state.js +++ b/lib/rules/no-did-update-set-state.js @@ -8,49 +8,55 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, - var mode = context.options[0] || 'allow-in-func'; + schema: [{ + enum: ['disallow-in-func'] + }] + }, - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + create: function(context) { - return { + var mode = context.options[0] || 'allow-in-func'; - CallExpression: function(node) { - var callee = node.callee; - if ( - callee.type !== 'MemberExpression' || - callee.object.type !== 'ThisExpression' || - callee.property.name !== 'setState' - ) { - return; - } - var ancestors = context.getAncestors(callee).reverse(); - var depth = 0; - for (var i = 0, j = ancestors.length; i < j; i++) { - if (/Function(Expression|Declaration)$/.test(ancestors[i].type)) { - depth++; - } + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + + CallExpression: function(node) { + var callee = node.callee; if ( - (ancestors[i].type !== 'Property' && ancestors[i].type !== 'MethodDefinition') || - ancestors[i].key.name !== 'componentDidUpdate' || - (mode !== 'disallow-in-func' && depth > 1) + callee.type !== 'MemberExpression' || + callee.object.type !== 'ThisExpression' || + callee.property.name !== 'setState' ) { - continue; + return; + } + var ancestors = context.getAncestors(callee).reverse(); + var depth = 0; + for (var i = 0, j = ancestors.length; i < j; i++) { + if (/Function(Expression|Declaration)$/.test(ancestors[i].type)) { + depth++; + } + if ( + (ancestors[i].type !== 'Property' && ancestors[i].type !== 'MethodDefinition') || + ancestors[i].key.name !== 'componentDidUpdate' || + (mode !== 'disallow-in-func' && depth > 1) + ) { + continue; + } + context.report({ + node: callee, + message: 'Do not use setState in componentDidUpdate' + }); + break; } - context.report({ - node: callee, - message: 'Do not use setState in componentDidUpdate' - }); - break; } - } - }; + }; + } }; - -module.exports.schema = [{ - enum: ['disallow-in-func'] -}]; diff --git a/lib/rules/no-direct-mutation-state.js b/lib/rules/no-direct-mutation-state.js index 556be0cced..97cdb7311f 100644 --- a/lib/rules/no-direct-mutation-state.js +++ b/lib/rules/no-direct-mutation-state.js @@ -10,69 +10,75 @@ var Components = require('../util/Components'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components, utils) { +module.exports = { + meta: { + docs: {} + }, - /** - * Checks if the component is valid - * @param {Object} component The component to process - * @returns {Boolean} True if the component is valid, false if not. - */ - function isValid(component) { - return Boolean(component && !component.mutateSetState); - } + create: Components.detect(function(context, components, utils) { - /** - * Reports undeclared proptypes for a given component - * @param {Object} component The component to process - */ - function reportMutations(component) { - var mutation; - for (var i = 0, j = component.mutations.length; i < j; i++) { - mutation = component.mutations[i]; - context.report({ - node: mutation, - message: 'Do not mutate state directly. Use setState().' - }); + /** + * Checks if the component is valid + * @param {Object} component The component to process + * @returns {Boolean} True if the component is valid, false if not. + */ + function isValid(component) { + return Boolean(component && !component.mutateSetState); } - } - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- - - return { - AssignmentExpression: function(node) { - var item; - if (!node.left || !node.left.object || !node.left.object.object) { - return; - } - item = node.left.object; - while (item.object.property) { - item = item.object; - } - if ( - item.object.type === 'ThisExpression' && - item.property.name === 'state' - ) { - var component = components.get(utils.getParentComponent()); - var mutations = component && component.mutations || []; - mutations.push(node.left.object); - components.set(node, { - mutateSetState: true, - mutations: mutations + /** + * Reports undeclared proptypes for a given component + * @param {Object} component The component to process + */ + function reportMutations(component) { + var mutation; + for (var i = 0, j = component.mutations.length; i < j; i++) { + mutation = component.mutations[i]; + context.report({ + node: mutation, + message: 'Do not mutate state directly. Use setState().' }); } - }, + } + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - 'Program:exit': function() { - var list = components.list(); - for (var component in list) { - if (!list.hasOwnProperty(component) || isValid(list[component])) { - continue; + return { + AssignmentExpression: function(node) { + var item; + if (!node.left || !node.left.object || !node.left.object.object) { + return; + } + item = node.left.object; + while (item.object.property) { + item = item.object; + } + if ( + item.object.type === 'ThisExpression' && + item.property.name === 'state' + ) { + var component = components.get(utils.getParentComponent()); + var mutations = component && component.mutations || []; + mutations.push(node.left.object); + components.set(node, { + mutateSetState: true, + mutations: mutations + }); + } + }, + + 'Program:exit': function() { + var list = components.list(); + for (var component in list) { + if (!list.hasOwnProperty(component) || isValid(list[component])) { + continue; + } + reportMutations(list[component]); } - reportMutations(list[component]); } - } - }; + }; -}); + }) +}; diff --git a/lib/rules/no-find-dom-node.js b/lib/rules/no-find-dom-node.js index f619f551dd..ce24fd1153 100644 --- a/lib/rules/no-find-dom-node.js +++ b/lib/rules/no-find-dom-node.js @@ -8,33 +8,38 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + create: function(context) { - return { + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - CallExpression: function(node) { - var callee = node.callee; + return { - var isfindDOMNode = - (callee.object && callee.object.callee && callee.object.callee.name === 'findDOMNode') || - (callee.property && callee.property.name === 'findDOMNode') - ; + CallExpression: function(node) { + var callee = node.callee; - if (!isfindDOMNode) { - return; - } + var isfindDOMNode = + (callee.object && callee.object.callee && callee.object.callee.name === 'findDOMNode') || + (callee.property && callee.property.name === 'findDOMNode') + ; - context.report({ - node: callee, - message: 'Do not use findDOMNode' - }); - } - }; + if (!isfindDOMNode) { + return; + } -}; + context.report({ + node: callee, + message: 'Do not use findDOMNode' + }); + } + }; -module.exports.schema = []; + } +}; diff --git a/lib/rules/no-is-mounted.js b/lib/rules/no-is-mounted.js index 850dab85e1..15df238ac6 100644 --- a/lib/rules/no-is-mounted.js +++ b/lib/rules/no-is-mounted.js @@ -8,35 +8,40 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + create: function(context) { - return { + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - CallExpression: function(node) { - var callee = node.callee; - if (callee.type !== 'MemberExpression') { - return; - } - if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'isMounted') { - return; - } - var ancestors = context.getAncestors(callee); - for (var i = 0, j = ancestors.length; i < j; i++) { - if (ancestors[i].type === 'Property' || ancestors[i].type === 'MethodDefinition') { - context.report({ - node: callee, - message: 'Do not use isMounted' - }); - break; + return { + + CallExpression: function(node) { + var callee = node.callee; + if (callee.type !== 'MemberExpression') { + return; + } + if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'isMounted') { + return; + } + var ancestors = context.getAncestors(callee); + for (var i = 0, j = ancestors.length; i < j; i++) { + if (ancestors[i].type === 'Property' || ancestors[i].type === 'MethodDefinition') { + context.report({ + node: callee, + message: 'Do not use isMounted' + }); + break; + } } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/no-multi-comp.js b/lib/rules/no-multi-comp.js index ae27bd63ff..b62867e071 100644 --- a/lib/rules/no-multi-comp.js +++ b/lib/rules/no-multi-comp.js @@ -10,55 +10,61 @@ var Components = require('../util/Components'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components) { +module.exports = { + meta: { + docs: {}, - var configuration = context.options[0] || {}; - var ignoreStateless = configuration.ignoreStateless || false; + schema: [{ + type: 'object', + properties: { + ignoreStateless: { + default: false, + type: 'boolean' + } + }, + additionalProperties: false + }] + }, - var MULTI_COMP_MESSAGE = 'Declare only one React component per file'; + create: Components.detect(function(context, components) { - /** - * Checks if the component is ignored - * @param {Object} component The component being checked. - * @returns {Boolean} True if the component is ignored, false if not. - */ - function isIgnored(component) { - return ignoreStateless === true && /Function/.test(component.node.type); - } + var configuration = context.options[0] || {}; + var ignoreStateless = configuration.ignoreStateless || false; - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + var MULTI_COMP_MESSAGE = 'Declare only one React component per file'; - return { - 'Program:exit': function() { - if (components.length() <= 1) { - return; - } + /** + * Checks if the component is ignored + * @param {Object} component The component being checked. + * @returns {Boolean} True if the component is ignored, false if not. + */ + function isIgnored(component) { + return ignoreStateless === true && /Function/.test(component.node.type); + } - var list = components.list(); - var i = 0; + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - for (var component in list) { - if (!list.hasOwnProperty(component) || isIgnored(list[component]) || ++i === 1) { - continue; + return { + 'Program:exit': function() { + if (components.length() <= 1) { + return; } - context.report({ - node: list[component].node, - message: MULTI_COMP_MESSAGE - }); - } - } - }; -}); -module.exports.schema = [{ - type: 'object', - properties: { - ignoreStateless: { - default: false, - type: 'boolean' - } - }, - additionalProperties: false -}]; + var list = components.list(); + var i = 0; + + for (var component in list) { + if (!list.hasOwnProperty(component) || isIgnored(list[component]) || ++i === 1) { + continue; + } + context.report({ + node: list[component].node, + message: MULTI_COMP_MESSAGE + }); + } + } + }; + }) +}; diff --git a/lib/rules/no-render-return-value.js b/lib/rules/no-render-return-value.js index cdc54ddb69..e82bea1145 100644 --- a/lib/rules/no-render-return-value.js +++ b/lib/rules/no-render-return-value.js @@ -10,53 +10,58 @@ var versionUtil = require('../util/version'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + create: function(context) { - return { + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - CallExpression: function(node) { - var callee = node.callee; - var parent = node.parent; - if (callee.type !== 'MemberExpression') { - return; - } + return { - var calleeObjectName = /^ReactDOM$/; - if (versionUtil.test(context, '15.0.0')) { - calleeObjectName = /^ReactDOM$/; - } else if (versionUtil.test(context, '0.14.0')) { - calleeObjectName = /^React(DOM)?$/; - } else if (versionUtil.test(context, '0.13.0')) { - calleeObjectName = /^React$/; - } + CallExpression: function(node) { + var callee = node.callee; + var parent = node.parent; + if (callee.type !== 'MemberExpression') { + return; + } - if ( - callee.object.type !== 'Identifier' || - !calleeObjectName.test(callee.object.name) || - callee.property.name !== 'render' - ) { - return; - } + var calleeObjectName = /^ReactDOM$/; + if (versionUtil.test(context, '15.0.0')) { + calleeObjectName = /^ReactDOM$/; + } else if (versionUtil.test(context, '0.14.0')) { + calleeObjectName = /^React(DOM)?$/; + } else if (versionUtil.test(context, '0.13.0')) { + calleeObjectName = /^React$/; + } - if ( - parent.type === 'VariableDeclarator' || - parent.type === 'Property' || - parent.type === 'ReturnStatement' || - parent.type === 'ArrowFunctionExpression' - ) { - context.report({ - node: callee, - message: 'Do not depend on the return value from ' + callee.object.name + '.render' - }); + if ( + callee.object.type !== 'Identifier' || + !calleeObjectName.test(callee.object.name) || + callee.property.name !== 'render' + ) { + return; + } + + if ( + parent.type === 'VariableDeclarator' || + parent.type === 'Property' || + parent.type === 'ReturnStatement' || + parent.type === 'ArrowFunctionExpression' + ) { + context.report({ + node: callee, + message: 'Do not depend on the return value from ' + callee.object.name + '.render' + }); + } } - } - }; + }; + } }; -module.exports.schema = []; - diff --git a/lib/rules/no-set-state.js b/lib/rules/no-set-state.js index ca8a6fb6f0..e4b284f79a 100644 --- a/lib/rules/no-set-state.js +++ b/lib/rules/no-set-state.js @@ -8,35 +8,40 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + create: function(context) { - return { + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - CallExpression: function(node) { - var callee = node.callee; - if (callee.type !== 'MemberExpression') { - return; - } - if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'setState') { - return; - } - var ancestors = context.getAncestors(callee); - for (var i = 0, j = ancestors.length; i < j; i++) { - if (ancestors[i].type === 'Property' || ancestors[i].type === 'MethodDefinition') { - context.report({ - node: callee, - message: 'Do not use setState' - }); - break; + return { + + CallExpression: function(node) { + var callee = node.callee; + if (callee.type !== 'MemberExpression') { + return; + } + if (callee.object.type !== 'ThisExpression' || callee.property.name !== 'setState') { + return; + } + var ancestors = context.getAncestors(callee); + for (var i = 0, j = ancestors.length; i < j; i++) { + if (ancestors[i].type === 'Property' || ancestors[i].type === 'MethodDefinition') { + context.report({ + node: callee, + message: 'Do not use setState' + }); + break; + } } } - } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/no-string-refs.js b/lib/rules/no-string-refs.js index 46c670645e..5ad2ee61ba 100644 --- a/lib/rules/no-string-refs.js +++ b/lib/rules/no-string-refs.js @@ -10,85 +10,90 @@ var Components = require('../util/Components'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components, utils) { - /** - * Checks if we are using refs - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are using refs, false if not. - */ - function isRefsUsage(node) { - return Boolean( - ( - utils.getParentES6Component() || - utils.getParentES5Component() - ) && - node.object.type === 'ThisExpression' && - node.property.name === 'refs' - ); - } +module.exports = { + meta: { + docs: {}, + schema: [] + }, - /** - * Checks if we are using a ref attribute - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are using a ref attribute, false if not. - */ - function isRefAttribute(node) { - return Boolean( - node.type === 'JSXAttribute' && - node.name && - node.name.name === 'ref' - ); - } + create: Components.detect(function(context, components, utils) { + /** + * Checks if we are using refs + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if we are using refs, false if not. + */ + function isRefsUsage(node) { + return Boolean( + ( + utils.getParentES6Component() || + utils.getParentES5Component() + ) && + node.object.type === 'ThisExpression' && + node.property.name === 'refs' + ); + } - /** - * Checks if a node contains a string value - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if the node contains a string value, false if not. - */ - function containsStringLiteral(node) { - return Boolean( - node.value && - node.value.type === 'Literal' && - typeof node.value.value === 'string' - ); - } + /** + * Checks if we are using a ref attribute + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if we are using a ref attribute, false if not. + */ + function isRefAttribute(node) { + return Boolean( + node.type === 'JSXAttribute' && + node.name && + node.name.name === 'ref' + ); + } - /** - * Checks if a node contains a string value within a jsx expression - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if the node contains a string value within a jsx expression, false if not. - */ - function containsStringExpressionContainer(node) { - return Boolean( - node.value && - node.value.type === 'JSXExpressionContainer' && - node.value.expression && - node.value.expression.type === 'Literal' && - typeof node.value.expression.value === 'string' - ); - } + /** + * Checks if a node contains a string value + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if the node contains a string value, false if not. + */ + function containsStringLiteral(node) { + return Boolean( + node.value && + node.value.type === 'Literal' && + typeof node.value.value === 'string' + ); + } - return { - MemberExpression: function(node) { - if (isRefsUsage(node)) { - context.report({ - node: node, - message: 'Using this.refs is deprecated.' - }); - } - }, - JSXAttribute: function(node) { - if ( - isRefAttribute(node) && - (containsStringLiteral(node) || containsStringExpressionContainer(node)) - ) { - context.report({ - node: node, - message: 'Using string literals in ref attributes is deprecated.' - }); - } + /** + * Checks if a node contains a string value within a jsx expression + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if the node contains a string value within a jsx expression, false if not. + */ + function containsStringExpressionContainer(node) { + return Boolean( + node.value && + node.value.type === 'JSXExpressionContainer' && + node.value.expression && + node.value.expression.type === 'Literal' && + typeof node.value.expression.value === 'string' + ); } - }; -}); -module.exports.schema = []; + return { + MemberExpression: function(node) { + if (isRefsUsage(node)) { + context.report({ + node: node, + message: 'Using this.refs is deprecated.' + }); + } + }, + JSXAttribute: function(node) { + if ( + isRefAttribute(node) && + (containsStringLiteral(node) || containsStringExpressionContainer(node)) + ) { + context.report({ + node: node, + message: 'Using string literals in ref attributes is deprecated.' + }); + } + } + }; + }) +}; diff --git a/lib/rules/no-unknown-property.js b/lib/rules/no-unknown-property.js index b1edd7bc8c..c59df44fbc 100644 --- a/lib/rules/no-unknown-property.js +++ b/lib/rules/no-unknown-property.js @@ -118,48 +118,55 @@ function getStandardName(context, name) { // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + fixable: 'code', + + schema: [{ + type: 'object', + properties: { + ignore: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + }] + }, - function getIgnoreConfig() { - return context.options[0] && context.options[0].ignore || DEFAULTS.ignore; - } + create: function(context) { - var sourceCode = context.getSourceCode(); + function getIgnoreConfig() { + return context.options[0] && context.options[0].ignore || DEFAULTS.ignore; + } - return { + var sourceCode = context.getSourceCode(); - JSXAttribute: function(node) { - var ignoreNames = getIgnoreConfig(); - var name = sourceCode.getText(node.name); - var standardName = getStandardName(context, name); - if (!isTagName(node) || !standardName || ignoreNames.indexOf(name) >= 0) { - return; - } - context.report({ - node: node, - message: UNKNOWN_MESSAGE, - data: { - name: name, - standardName: standardName - }, - fix: function(fixer) { - return fixer.replaceText(node.name, standardName); + return { + + JSXAttribute: function(node) { + var ignoreNames = getIgnoreConfig(); + var name = sourceCode.getText(node.name); + var standardName = getStandardName(context, name); + if (!isTagName(node) || !standardName || ignoreNames.indexOf(name) >= 0) { + return; } - }); - } - }; + context.report({ + node: node, + message: UNKNOWN_MESSAGE, + data: { + name: name, + standardName: standardName + }, + fix: function(fixer) { + return fixer.replaceText(node.name, standardName); + } + }); + } + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - ignore: { - type: 'array', - items: { - type: 'string' - } - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/prefer-es6-class.js b/lib/rules/prefer-es6-class.js index 966fea1bb0..1df3113c7c 100644 --- a/lib/rules/prefer-es6-class.js +++ b/lib/rules/prefer-es6-class.js @@ -10,29 +10,35 @@ var Components = require('../util/Components'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components, utils) { - var configuration = context.options[0] || 'always'; +module.exports = { + meta: { + docs: {}, - return { - ObjectExpression: function(node) { - if (utils.isES5Component(node) && configuration === 'always') { - context.report({ - node: node, - message: 'Component should use es6 class instead of createClass' - }); - } - }, - ClassDeclaration: function(node) { - if (utils.isES6Component(node) && configuration === 'never') { - context.report({ - node: node, - message: 'Component should use createClass instead of es6 class' - }); - } - } - }; -}); + schema: [{ + enum: ['always', 'never'] + }] + }, -module.exports.schema = [{ - enum: ['always', 'never'] -}]; + create: Components.detect(function(context, components, utils) { + var configuration = context.options[0] || 'always'; + + return { + ObjectExpression: function(node) { + if (utils.isES5Component(node) && configuration === 'always') { + context.report({ + node: node, + message: 'Component should use es6 class instead of createClass' + }); + } + }, + ClassDeclaration: function(node) { + if (utils.isES6Component(node) && configuration === 'never') { + context.report({ + node: node, + message: 'Component should use createClass instead of es6 class' + }); + } + } + }; + }) +}; diff --git a/lib/rules/prefer-stateless-function.js b/lib/rules/prefer-stateless-function.js index 2a21b53d12..ec68b6830f 100644 --- a/lib/rules/prefer-stateless-function.js +++ b/lib/rules/prefer-stateless-function.js @@ -13,313 +13,318 @@ var versionUtil = require('../util/version'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components, utils) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - var sourceCode = context.getSourceCode(); + create: Components.detect(function(context, components, utils) { - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + var sourceCode = context.getSourceCode(); - /** - * Get properties name - * @param {Object} node - Property. - * @returns {String} Property name. - */ - function getPropertyName(node) { - // Special case for class properties - // (babel-eslint does not expose property name so we have to rely on tokens) - if (node.type === 'ClassProperty') { - var tokens = context.getFirstTokens(node, 2); - return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; - } + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - return node.key.name; - } + /** + * Get properties name + * @param {Object} node - Property. + * @returns {String} Property name. + */ + function getPropertyName(node) { + // Special case for class properties + // (babel-eslint does not expose property name so we have to rely on tokens) + if (node.type === 'ClassProperty') { + var tokens = context.getFirstTokens(node, 2); + return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; + } - /** - * Get properties for a given AST node - * @param {ASTNode} node The AST node being checked. - * @returns {Array} Properties array. - */ - function getComponentProperties(node) { - switch (node.type) { - case 'ClassExpression': - case 'ClassDeclaration': - return node.body.body; - case 'ObjectExpression': - return node.properties; - default: - return []; + return node.key.name; } - } - /** - * Checks whether a given array of statements is a single call of `super`. - * @see ESLint no-useless-constructor rule - * @param {ASTNode[]} body - An array of statements to check. - * @returns {boolean} `true` if the body is a single call of `super`. - */ - function isSingleSuperCall(body) { - return ( - body.length === 1 && - body[0].type === 'ExpressionStatement' && - body[0].expression.type === 'CallExpression' && - body[0].expression.callee.type === 'Super' - ); - } + /** + * Get properties for a given AST node + * @param {ASTNode} node The AST node being checked. + * @returns {Array} Properties array. + */ + function getComponentProperties(node) { + switch (node.type) { + case 'ClassExpression': + case 'ClassDeclaration': + return node.body.body; + case 'ObjectExpression': + return node.properties; + default: + return []; + } + } - /** - * Checks whether a given node is a pattern which doesn't have any side effects. - * Default parameters and Destructuring parameters can have side effects. - * @see ESLint no-useless-constructor rule - * @param {ASTNode} node - A pattern node. - * @returns {boolean} `true` if the node doesn't have any side effects. - */ - function isSimple(node) { - return node.type === 'Identifier' || node.type === 'RestElement'; - } + /** + * Checks whether a given array of statements is a single call of `super`. + * @see ESLint no-useless-constructor rule + * @param {ASTNode[]} body - An array of statements to check. + * @returns {boolean} `true` if the body is a single call of `super`. + */ + function isSingleSuperCall(body) { + return ( + body.length === 1 && + body[0].type === 'ExpressionStatement' && + body[0].expression.type === 'CallExpression' && + body[0].expression.callee.type === 'Super' + ); + } - /** - * Checks whether a given array of expressions is `...arguments` or not. - * `super(...arguments)` passes all arguments through. - * @see ESLint no-useless-constructor rule - * @param {ASTNode[]} superArgs - An array of expressions to check. - * @returns {boolean} `true` if the superArgs is `...arguments`. - */ - function isSpreadArguments(superArgs) { - return ( - superArgs.length === 1 && - superArgs[0].type === 'SpreadElement' && - superArgs[0].argument.type === 'Identifier' && - superArgs[0].argument.name === 'arguments' - ); - } + /** + * Checks whether a given node is a pattern which doesn't have any side effects. + * Default parameters and Destructuring parameters can have side effects. + * @see ESLint no-useless-constructor rule + * @param {ASTNode} node - A pattern node. + * @returns {boolean} `true` if the node doesn't have any side effects. + */ + function isSimple(node) { + return node.type === 'Identifier' || node.type === 'RestElement'; + } - /** - * Checks whether given 2 nodes are identifiers which have the same name or not. - * @see ESLint no-useless-constructor rule - * @param {ASTNode} ctorParam - A node to check. - * @param {ASTNode} superArg - A node to check. - * @returns {boolean} `true` if the nodes are identifiers which have the same - * name. - */ - function isValidIdentifierPair(ctorParam, superArg) { - return ( - ctorParam.type === 'Identifier' && - superArg.type === 'Identifier' && - ctorParam.name === superArg.name - ); - } + /** + * Checks whether a given array of expressions is `...arguments` or not. + * `super(...arguments)` passes all arguments through. + * @see ESLint no-useless-constructor rule + * @param {ASTNode[]} superArgs - An array of expressions to check. + * @returns {boolean} `true` if the superArgs is `...arguments`. + */ + function isSpreadArguments(superArgs) { + return ( + superArgs.length === 1 && + superArgs[0].type === 'SpreadElement' && + superArgs[0].argument.type === 'Identifier' && + superArgs[0].argument.name === 'arguments' + ); + } - /** - * Checks whether given 2 nodes are a rest/spread pair which has the same values. - * @see ESLint no-useless-constructor rule - * @param {ASTNode} ctorParam - A node to check. - * @param {ASTNode} superArg - A node to check. - * @returns {boolean} `true` if the nodes are a rest/spread pair which has the - * same values. - */ - function isValidRestSpreadPair(ctorParam, superArg) { - return ( - ctorParam.type === 'RestElement' && - superArg.type === 'SpreadElement' && - isValidIdentifierPair(ctorParam.argument, superArg.argument) - ); - } + /** + * Checks whether given 2 nodes are identifiers which have the same name or not. + * @see ESLint no-useless-constructor rule + * @param {ASTNode} ctorParam - A node to check. + * @param {ASTNode} superArg - A node to check. + * @returns {boolean} `true` if the nodes are identifiers which have the same + * name. + */ + function isValidIdentifierPair(ctorParam, superArg) { + return ( + ctorParam.type === 'Identifier' && + superArg.type === 'Identifier' && + ctorParam.name === superArg.name + ); + } - /** - * Checks whether given 2 nodes have the same value or not. - * @see ESLint no-useless-constructor rule - * @param {ASTNode} ctorParam - A node to check. - * @param {ASTNode} superArg - A node to check. - * @returns {boolean} `true` if the nodes have the same value or not. - */ - function isValidPair(ctorParam, superArg) { - return ( - isValidIdentifierPair(ctorParam, superArg) || - isValidRestSpreadPair(ctorParam, superArg) - ); - } + /** + * Checks whether given 2 nodes are a rest/spread pair which has the same values. + * @see ESLint no-useless-constructor rule + * @param {ASTNode} ctorParam - A node to check. + * @param {ASTNode} superArg - A node to check. + * @returns {boolean} `true` if the nodes are a rest/spread pair which has the + * same values. + */ + function isValidRestSpreadPair(ctorParam, superArg) { + return ( + ctorParam.type === 'RestElement' && + superArg.type === 'SpreadElement' && + isValidIdentifierPair(ctorParam.argument, superArg.argument) + ); + } - /** - * Checks whether the parameters of a constructor and the arguments of `super()` - * have the same values or not. - * @see ESLint no-useless-constructor rule - * @param {ASTNode} ctorParams - The parameters of a constructor to check. - * @param {ASTNode} superArgs - The arguments of `super()` to check. - * @returns {boolean} `true` if those have the same values. - */ - function isPassingThrough(ctorParams, superArgs) { - if (ctorParams.length !== superArgs.length) { - return false; + /** + * Checks whether given 2 nodes have the same value or not. + * @see ESLint no-useless-constructor rule + * @param {ASTNode} ctorParam - A node to check. + * @param {ASTNode} superArg - A node to check. + * @returns {boolean} `true` if the nodes have the same value or not. + */ + function isValidPair(ctorParam, superArg) { + return ( + isValidIdentifierPair(ctorParam, superArg) || + isValidRestSpreadPair(ctorParam, superArg) + ); } - for (var i = 0; i < ctorParams.length; ++i) { - if (!isValidPair(ctorParams[i], superArgs[i])) { + /** + * Checks whether the parameters of a constructor and the arguments of `super()` + * have the same values or not. + * @see ESLint no-useless-constructor rule + * @param {ASTNode} ctorParams - The parameters of a constructor to check. + * @param {ASTNode} superArgs - The arguments of `super()` to check. + * @returns {boolean} `true` if those have the same values. + */ + function isPassingThrough(ctorParams, superArgs) { + if (ctorParams.length !== superArgs.length) { return false; } - } - return true; - } + for (var i = 0; i < ctorParams.length; ++i) { + if (!isValidPair(ctorParams[i], superArgs[i])) { + return false; + } + } - /** - * Checks whether the constructor body is a redundant super call. - * @see ESLint no-useless-constructor rule - * @param {Array} body - constructor body content. - * @param {Array} ctorParams - The params to check against super call. - * @returns {boolean} true if the construtor body is redundant - */ - function isRedundantSuperCall(body, ctorParams) { - return ( - isSingleSuperCall(body) && - ctorParams.every(isSimple) && - ( - isSpreadArguments(body[0].expression.arguments) || - isPassingThrough(ctorParams, body[0].expression.arguments) - ) - ); - } + return true; + } - /** - * Check if a given AST node have any other properties the ones available in stateless components - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if the node has at least one other property, false if not. - */ - function hasOtherProperties(node) { - var properties = getComponentProperties(node); - return properties.some(function(property) { - var name = getPropertyName(property); - var isDisplayName = name === 'displayName'; - var isPropTypes = name === 'propTypes' || name === 'props' && property.typeAnnotation; - var contextTypes = name === 'contextTypes'; - var isUselessConstructor = - property.kind === 'constructor' && - isRedundantSuperCall(property.value.body.body, property.value.params) - ; - var isRender = name === 'render'; - return !isDisplayName && !isPropTypes && !contextTypes && !isUselessConstructor && !isRender; - }); - } + /** + * Checks whether the constructor body is a redundant super call. + * @see ESLint no-useless-constructor rule + * @param {Array} body - constructor body content. + * @param {Array} ctorParams - The params to check against super call. + * @returns {boolean} true if the construtor body is redundant + */ + function isRedundantSuperCall(body, ctorParams) { + return ( + isSingleSuperCall(body) && + ctorParams.every(isSimple) && + ( + isSpreadArguments(body[0].expression.arguments) || + isPassingThrough(ctorParams, body[0].expression.arguments) + ) + ); + } - /** - * Mark a setState as used - * @param {ASTNode} node The AST node being checked. - */ - function markThisAsUsed(node) { - components.set(node, { - useThis: true - }); - } + /** + * Check if a given AST node have any other properties the ones available in stateless components + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if the node has at least one other property, false if not. + */ + function hasOtherProperties(node) { + var properties = getComponentProperties(node); + return properties.some(function(property) { + var name = getPropertyName(property); + var isDisplayName = name === 'displayName'; + var isPropTypes = name === 'propTypes' || name === 'props' && property.typeAnnotation; + var contextTypes = name === 'contextTypes'; + var isUselessConstructor = + property.kind === 'constructor' && + isRedundantSuperCall(property.value.body.body, property.value.params) + ; + var isRender = name === 'render'; + return !isDisplayName && !isPropTypes && !contextTypes && !isUselessConstructor && !isRender; + }); + } - /** - * Mark a ref as used - * @param {ASTNode} node The AST node being checked. - */ - function markRefAsUsed(node) { - components.set(node, { - useRef: true - }); - } + /** + * Mark a setState as used + * @param {ASTNode} node The AST node being checked. + */ + function markThisAsUsed(node) { + components.set(node, { + useThis: true + }); + } - /** - * Mark return as invalid - * @param {ASTNode} node The AST node being checked. - */ - function markReturnAsInvalid(node) { - components.set(node, { - invalidReturn: true - }); - } + /** + * Mark a ref as used + * @param {ASTNode} node The AST node being checked. + */ + function markRefAsUsed(node) { + components.set(node, { + useRef: true + }); + } - return { - // Mark `this` destructuring as a usage of `this` - VariableDeclarator: function(node) { - // Ignore destructuring on other than `this` - if (!node.id || node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'ThisExpression') { - return; - } - // Ignore `props` and `context` - var useThis = node.id.properties.some(function(property) { - var name = getPropertyName(property); - return name !== 'props' && name !== 'context'; + /** + * Mark return as invalid + * @param {ASTNode} node The AST node being checked. + */ + function markReturnAsInvalid(node) { + components.set(node, { + invalidReturn: true }); - if (!useThis) { - return; - } - markThisAsUsed(node); - }, + } - // Mark `this` usage - MemberExpression: function(node) { - // Ignore calls to `this.props` and `this.context` - if ( - node.object.type !== 'ThisExpression' || - (node.property.name || node.property.value) === 'props' || - (node.property.name || node.property.value) === 'context' - ) { - return; - } - markThisAsUsed(node); - }, + return { + // Mark `this` destructuring as a usage of `this` + VariableDeclarator: function(node) { + // Ignore destructuring on other than `this` + if (!node.id || node.id.type !== 'ObjectPattern' || !node.init || node.init.type !== 'ThisExpression') { + return; + } + // Ignore `props` and `context` + var useThis = node.id.properties.some(function(property) { + var name = getPropertyName(property); + return name !== 'props' && name !== 'context'; + }); + if (!useThis) { + return; + } + markThisAsUsed(node); + }, - // Mark `ref` usage - JSXAttribute: function(node) { - var name = sourceCode.getText(node.name); - if (name !== 'ref') { - return; - } - markRefAsUsed(node); - }, + // Mark `this` usage + MemberExpression: function(node) { + // Ignore calls to `this.props` and `this.context` + if ( + node.object.type !== 'ThisExpression' || + (node.property.name || node.property.value) === 'props' || + (node.property.name || node.property.value) === 'context' + ) { + return; + } + markThisAsUsed(node); + }, - // Mark `render` that do not return some JSX - ReturnStatement: function(node) { - var blockNode; - var scope = context.getScope(); - while (scope) { - blockNode = scope.block && scope.block.parent; - if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) { - break; + // Mark `ref` usage + JSXAttribute: function(node) { + var name = sourceCode.getText(node.name); + if (name !== 'ref') { + return; } - scope = scope.upper; - } - var isRender = blockNode && blockNode.key && blockNode.key.name === 'render'; - var allowNull = versionUtil.test(context, '15.0.0'); // Stateless components can return null since React 15 - var isReturningJSX = utils.isReturningJSX(node, !allowNull); - var isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false); - if ( - !isRender || - (allowNull && (isReturningJSX || isReturningNull)) || - (!allowNull && isReturningJSX) - ) { - return; - } - markReturnAsInvalid(node); - }, + markRefAsUsed(node); + }, - 'Program:exit': function() { - var list = components.list(); - for (var component in list) { + // Mark `render` that do not return some JSX + ReturnStatement: function(node) { + var blockNode; + var scope = context.getScope(); + while (scope) { + blockNode = scope.block && scope.block.parent; + if (blockNode && (blockNode.type === 'MethodDefinition' || blockNode.type === 'Property')) { + break; + } + scope = scope.upper; + } + var isRender = blockNode && blockNode.key && blockNode.key.name === 'render'; + var allowNull = versionUtil.test(context, '15.0.0'); // Stateless components can return null since React 15 + var isReturningJSX = utils.isReturningJSX(node, !allowNull); + var isReturningNull = node.argument && (node.argument.value === null || node.argument.value === false); if ( - !list.hasOwnProperty(component) || - hasOtherProperties(list[component].node) || - list[component].useThis || - list[component].useRef || - list[component].invalidReturn || - (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) + !isRender || + (allowNull && (isReturningJSX || isReturningNull)) || + (!allowNull && isReturningJSX) ) { - continue; + return; } + markReturnAsInvalid(node); + }, - context.report({ - node: list[component].node, - message: 'Component should be written as a pure function' - }); - } - } - }; + 'Program:exit': function() { + var list = components.list(); + for (var component in list) { + if ( + !list.hasOwnProperty(component) || + hasOtherProperties(list[component].node) || + list[component].useThis || + list[component].useRef || + list[component].invalidReturn || + (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) + ) { + continue; + } -}); + context.report({ + node: list[component].node, + message: 'Component should be written as a pure function' + }); + } + } + }; -module.exports.schema = []; + }) +}; diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index d047f04667..9ef037f52d 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -20,342 +20,426 @@ var DIRECT_PROPS_REGEX = /^props\s*(\.|\[)/; // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components, utils) { - - var sourceCode = context.getSourceCode(); - var configuration = context.options[0] || {}; - var ignored = configuration.ignore || []; - var customValidators = configuration.customValidators || []; - // Used to track the type annotations in scope. - // Necessary because babel's scopes do not track type annotations. - var stack = null; - - var MISSING_MESSAGE = '\'{{name}}\' is missing in props validation'; - - /** - * Helper for accessing the current scope in the stack. - * @param {string} key The name of the identifier to access. If omitted, returns the full scope. - * @param {ASTNode} value If provided sets the new value for the identifier. - * @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier. - */ - function typeScope(key, value) { - if (arguments.length === 0) { - return stack[stack.length - 1]; - } else if (arguments.length === 1) { - return stack[stack.length - 1][key]; - } - stack[stack.length - 1][key] = value; - return value; - } - - /** - * Checks if we are using a prop - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are using a prop, false if not. - */ - function isPropTypesUsage(node) { - var isClassUsage = ( - (utils.getParentES6Component() || utils.getParentES5Component()) && - node.object.type === 'ThisExpression' && node.property.name === 'props' - ); - var isStatelessFunctionUsage = node.object.name === 'props'; - return isClassUsage || isStatelessFunctionUsage; - } - - /** - * Checks if we are declaring a `props` class property with a flow type annotation. - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if the node is a type annotated props declaration, false if not. - */ - function isAnnotatedClassPropsDeclaration(node) { - if (node && node.type === 'ClassProperty') { - var tokens = context.getFirstTokens(node, 2); - if ( - node.typeAnnotation && ( - tokens[0].value === 'props' || - (tokens[1] && tokens[1].value === 'props') - ) - ) { - return true; +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: { + ignore: { + type: 'array', + items: { + type: 'string' + } + }, + customValidators: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + }] + }, + + create: Components.detect(function(context, components, utils) { + + var sourceCode = context.getSourceCode(); + var configuration = context.options[0] || {}; + var ignored = configuration.ignore || []; + var customValidators = configuration.customValidators || []; + // Used to track the type annotations in scope. + // Necessary because babel's scopes do not track type annotations. + var stack = null; + + var MISSING_MESSAGE = '\'{{name}}\' is missing in props validation'; + + /** + * Helper for accessing the current scope in the stack. + * @param {string} key The name of the identifier to access. If omitted, returns the full scope. + * @param {ASTNode} value If provided sets the new value for the identifier. + * @returns {Object|ASTNode} Either the whole scope or the ASTNode associated with the given identifier. + */ + function typeScope(key, value) { + if (arguments.length === 0) { + return stack[stack.length - 1]; + } else if (arguments.length === 1) { + return stack[stack.length - 1][key]; } + stack[stack.length - 1][key] = value; + return value; } - return false; - } - - /** - * Checks if we are declaring a `props` argument with a flow type annotation. - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if the node is a type annotated props declaration, false if not. - */ - function isAnnotatedFunctionPropsDeclaration(node) { - if (node && node.params && node.params.length) { - var tokens = context.getFirstTokens(node.params[0], 2); - var isAnnotated = node.params[0].typeAnnotation; - var isDestructuredProps = node.params[0].type === 'ObjectPattern'; - var isProps = tokens[0].value === 'props' || (tokens[1] && tokens[1].value === 'props'); - if (isAnnotated && (isDestructuredProps || isProps)) { - return true; + + /** + * Checks if we are using a prop + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if we are using a prop, false if not. + */ + function isPropTypesUsage(node) { + var isClassUsage = ( + (utils.getParentES6Component() || utils.getParentES5Component()) && + node.object.type === 'ThisExpression' && node.property.name === 'props' + ); + var isStatelessFunctionUsage = node.object.name === 'props'; + return isClassUsage || isStatelessFunctionUsage; + } + + /** + * Checks if we are declaring a `props` class property with a flow type annotation. + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if the node is a type annotated props declaration, false if not. + */ + function isAnnotatedClassPropsDeclaration(node) { + if (node && node.type === 'ClassProperty') { + var tokens = context.getFirstTokens(node, 2); + if ( + node.typeAnnotation && ( + tokens[0].value === 'props' || + (tokens[1] && tokens[1].value === 'props') + ) + ) { + return true; + } } + return false; } - return false; - } - - /** - * Checks if we are declaring a prop - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are declaring a prop, false if not. - */ - function isPropTypesDeclaration(node) { - - // Special case for class properties - // (babel-eslint does not expose property name so we have to rely on tokens) - if (node && node.type === 'ClassProperty') { - var tokens = context.getFirstTokens(node, 2); - if ( - tokens[0].value === 'propTypes' || - (tokens[1] && tokens[1].value === 'propTypes') - ) { - return true; + + /** + * Checks if we are declaring a `props` argument with a flow type annotation. + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if the node is a type annotated props declaration, false if not. + */ + function isAnnotatedFunctionPropsDeclaration(node) { + if (node && node.params && node.params.length) { + var tokens = context.getFirstTokens(node.params[0], 2); + var isAnnotated = node.params[0].typeAnnotation; + var isDestructuredProps = node.params[0].type === 'ObjectPattern'; + var isProps = tokens[0].value === 'props' || (tokens[1] && tokens[1].value === 'props'); + if (isAnnotated && (isDestructuredProps || isProps)) { + return true; + } } return false; } - return Boolean( - node && - node.name === 'propTypes' - ); - - } - - /** - * Checks if the prop is ignored - * @param {String} name Name of the prop to check. - * @returns {Boolean} True if the prop is ignored, false if not. - */ - function isIgnored(name) { - return ignored.indexOf(name) !== -1; - } - - /** - * Checks if prop should be validated by plugin-react-proptypes - * @param {String} validator Name of validator to check. - * @returns {Boolean} True if validator should be checked by custom validator. - */ - function hasCustomValidator(validator) { - return customValidators.indexOf(validator) !== -1; - } - - /** - * Checks if the component must be validated - * @param {Object} component The component to process - * @returns {Boolean} True if the component must be validated, false if not. - */ - function mustBeValidated(component) { - return Boolean( - component && - component.usedPropTypes && - !component.ignorePropsValidation - ); - } - - /** - * Internal: Checks if the prop is declared - * @param {Object} declaredPropTypes Description of propTypes declared in the current component - * @param {String[]} keyList Dot separated name of the prop to check. - * @returns {Boolean} True if the prop is declared, false if not. - */ - function _isDeclaredInComponent(declaredPropTypes, keyList) { - for (var i = 0, j = keyList.length; i < j; i++) { - var key = keyList[i]; - var propType = ( - declaredPropTypes && ( - // Check if this key is declared - declaredPropTypes[key] || - // If not, check if this type accepts any key - declaredPropTypes.__ANY_KEY__ - ) + /** + * Checks if we are declaring a prop + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if we are declaring a prop, false if not. + */ + function isPropTypesDeclaration(node) { + + // Special case for class properties + // (babel-eslint does not expose property name so we have to rely on tokens) + if (node && node.type === 'ClassProperty') { + var tokens = context.getFirstTokens(node, 2); + if ( + tokens[0].value === 'propTypes' || + (tokens[1] && tokens[1].value === 'propTypes') + ) { + return true; + } + return false; + } + + return Boolean( + node && + node.name === 'propTypes' ); - if (!propType) { - // If it's a computed property, we can't make any further analysis, but is valid - return key === '__COMPUTED_PROP__'; - } - if (propType === true) { - return true; - } - // Consider every children as declared - if (propType.children === true) { - return true; - } - if (propType.acceptedProperties) { - return key in propType.acceptedProperties; - } - if (propType.type === 'union') { - // If we fall in this case, we know there is at least one complex type in the union - if (i + 1 >= j) { - // this is the last key, accept everything + } + + /** + * Checks if the prop is ignored + * @param {String} name Name of the prop to check. + * @returns {Boolean} True if the prop is ignored, false if not. + */ + function isIgnored(name) { + return ignored.indexOf(name) !== -1; + } + + /** + * Checks if prop should be validated by plugin-react-proptypes + * @param {String} validator Name of validator to check. + * @returns {Boolean} True if validator should be checked by custom validator. + */ + function hasCustomValidator(validator) { + return customValidators.indexOf(validator) !== -1; + } + + /** + * Checks if the component must be validated + * @param {Object} component The component to process + * @returns {Boolean} True if the component must be validated, false if not. + */ + function mustBeValidated(component) { + return Boolean( + component && + component.usedPropTypes && + !component.ignorePropsValidation + ); + } + + /** + * Internal: Checks if the prop is declared + * @param {Object} declaredPropTypes Description of propTypes declared in the current component + * @param {String[]} keyList Dot separated name of the prop to check. + * @returns {Boolean} True if the prop is declared, false if not. + */ + function _isDeclaredInComponent(declaredPropTypes, keyList) { + for (var i = 0, j = keyList.length; i < j; i++) { + var key = keyList[i]; + var propType = ( + declaredPropTypes && ( + // Check if this key is declared + (declaredPropTypes[key] || // If not, check if this type accepts any key + declaredPropTypes.__ANY_KEY__) + ) + ); + + if (!propType) { + // If it's a computed property, we can't make any further analysis, but is valid + return key === '__COMPUTED_PROP__'; + } + if (propType === true) { + return true; + } + // Consider every children as declared + if (propType.children === true) { return true; } - // non trivial, check all of them - var unionTypes = propType.children; - var unionPropType = {}; - for (var k = 0, z = unionTypes.length; k < z; k++) { - unionPropType[key] = unionTypes[k]; - var isValid = _isDeclaredInComponent( - unionPropType, - keyList.slice(i) - ); - if (isValid) { + if (propType.acceptedProperties) { + return key in propType.acceptedProperties; + } + if (propType.type === 'union') { + // If we fall in this case, we know there is at least one complex type in the union + if (i + 1 >= j) { + // this is the last key, accept everything return true; } - } + // non trivial, check all of them + var unionTypes = propType.children; + var unionPropType = {}; + for (var k = 0, z = unionTypes.length; k < z; k++) { + unionPropType[key] = unionTypes[k]; + var isValid = _isDeclaredInComponent( + unionPropType, + keyList.slice(i) + ); + if (isValid) { + return true; + } + } - // every possible union were invalid - return false; + // every possible union were invalid + return false; + } + declaredPropTypes = propType.children; } - declaredPropTypes = propType.children; + return true; } - return true; - } - - /** - * Checks if the prop is declared - * @param {ASTNode} node The AST node being checked. - * @param {String[]} names List of names of the prop to check. - * @returns {Boolean} True if the prop is declared, false if not. - */ - function isDeclaredInComponent(node, names) { - while (node) { - var component = components.get(node); - var isDeclared = - component && component.confidence === 2 && - _isDeclaredInComponent(component.declaredPropTypes || {}, names) - ; - if (isDeclared) { - return true; + + /** + * Checks if the prop is declared + * @param {ASTNode} node The AST node being checked. + * @param {String[]} names List of names of the prop to check. + * @returns {Boolean} True if the prop is declared, false if not. + */ + function isDeclaredInComponent(node, names) { + while (node) { + var component = components.get(node); + var isDeclared = + component && component.confidence === 2 && + _isDeclaredInComponent(component.declaredPropTypes || {}, names) + ; + if (isDeclared) { + return true; + } + node = node.parent; } - node = node.parent; + return false; } - return false; - } - - /** - * Checks if the prop has spread operator. - * @param {ASTNode} node The AST node being marked. - * @returns {Boolean} True if the prop has spread operator, false if not. - */ - function hasSpreadOperator(node) { - var tokens = sourceCode.getTokens(node); - return tokens.length && tokens[0].value === '...'; - } - - /** - * Retrieve the name of a key node - * @param {ASTNode} node The AST node with the key. - * @return {string} the name of the key - */ - function getKeyValue(node) { - if (node.type === 'ObjectTypeProperty') { - var tokens = context.getFirstTokens(node, 1); - return tokens[0].value; + + /** + * Checks if the prop has spread operator. + * @param {ASTNode} node The AST node being marked. + * @returns {Boolean} True if the prop has spread operator, false if not. + */ + function hasSpreadOperator(node) { + var tokens = sourceCode.getTokens(node); + return tokens.length && tokens[0].value === '...'; } - var key = node.key || node.argument; - return key.type === 'Identifier' ? key.name : key.value; - } - - /** - * Iterates through a properties node, like a customized forEach. - * @param {Object[]} properties Array of properties to iterate. - * @param {Function} fn Function to call on each property, receives property key - and property value. (key, value) => void - */ - function iterateProperties(properties, fn) { - if (properties.length && typeof fn === 'function') { - for (var i = 0, j = properties.length; i < j; i++) { - var node = properties[i]; - var key = getKeyValue(node); - - var value = node.value; - fn(key, value); + + /** + * Retrieve the name of a key node + * @param {ASTNode} node The AST node with the key. + * @return {string} the name of the key + */ + function getKeyValue(node) { + if (node.type === 'ObjectTypeProperty') { + var tokens = context.getFirstTokens(node, 1); + return tokens[0].value; } - } - } - - /** - * Creates the representation of the React propTypes for the component. - * The representation is used to verify nested used properties. - * @param {ASTNode} value Node of the React.PropTypes for the desired property - * @return {Object|Boolean} The representation of the declaration, true means - * the property is declared without the need for further analysis. - */ - function buildReactDeclarationTypes(value) { - if ( - value && - value.callee && - value.callee.object && - hasCustomValidator(value.callee.object.name) - ) { - return true; + var key = node.key || node.argument; + return key.type === 'Identifier' ? key.name : key.value; } - if ( - value && - value.type === 'MemberExpression' && - value.property && - value.property.name && - value.property.name === 'isRequired' - ) { - value = value.object; + /** + * Iterates through a properties node, like a customized forEach. + * @param {Object[]} properties Array of properties to iterate. + * @param {Function} fn Function to call on each property, receives property key + and property value. (key, value) => void + */ + function iterateProperties(properties, fn) { + if (properties.length && typeof fn === 'function') { + for (var i = 0, j = properties.length; i < j; i++) { + var node = properties[i]; + var key = getKeyValue(node); + + var value = node.value; + fn(key, value); + } + } } - // Verify React.PropTypes that are functions - if ( - value && - value.type === 'CallExpression' && - value.callee && - value.callee.property && - value.callee.property.name && - value.arguments && - value.arguments.length > 0 - ) { - var callName = value.callee.property.name; - var argument = value.arguments[0]; - switch (callName) { - case 'shape': - if (argument.type !== 'ObjectExpression') { - // Invalid proptype or cannot analyse statically + /** + * Creates the representation of the React propTypes for the component. + * The representation is used to verify nested used properties. + * @param {ASTNode} value Node of the React.PropTypes for the desired property + * @return {Object|Boolean} The representation of the declaration, true means + * the property is declared without the need for further analysis. + */ + function buildReactDeclarationTypes(value) { + if ( + value && + value.callee && + value.callee.object && + hasCustomValidator(value.callee.object.name) + ) { + return true; + } + + if ( + value && + value.type === 'MemberExpression' && + value.property && + value.property.name && + value.property.name === 'isRequired' + ) { + value = value.object; + } + + // Verify React.PropTypes that are functions + if ( + value && + value.type === 'CallExpression' && + value.callee && + value.callee.property && + value.callee.property.name && + value.arguments && + value.arguments.length > 0 + ) { + var callName = value.callee.property.name; + var argument = value.arguments[0]; + switch (callName) { + case 'shape': + if (argument.type !== 'ObjectExpression') { + // Invalid proptype or cannot analyse statically + return true; + } + var shapeTypeDefinition = { + type: 'shape', + children: {} + }; + iterateProperties(argument.properties, function(childKey, childValue) { + shapeTypeDefinition.children[childKey] = buildReactDeclarationTypes(childValue); + }); + return shapeTypeDefinition; + case 'arrayOf': + case 'objectOf': + return { + type: 'object', + children: { + __ANY_KEY__: buildReactDeclarationTypes(argument) + } + }; + case 'oneOfType': + if ( + !argument.elements || + !argument.elements.length + ) { + // Invalid proptype or cannot analyse statically + return true; + } + var unionTypeDefinition = { + type: 'union', + children: [] + }; + for (var i = 0, j = argument.elements.length; i < j; i++) { + var type = buildReactDeclarationTypes(argument.elements[i]); + // keep only complex type + if (type !== true) { + if (type.children === true) { + // every child is accepted for one type, abort type analysis + unionTypeDefinition.children = true; + return unionTypeDefinition; + } + } + + unionTypeDefinition.children.push(type); + } + if (unionTypeDefinition.length === 0) { + // no complex type found, simply accept everything + return true; + } + return unionTypeDefinition; + case 'instanceOf': + return { + type: 'instance', + // Accept all children because we can't know what type they are + children: true + }; + case 'oneOf': + default: return true; + } + } + // Unknown property or accepts everything (any, object, ...) + return true; + } + + /** + * Creates the representation of the React props type annotation for the component. + * The representation is used to verify nested used properties. + * @param {ASTNode} annotation Type annotation for the props class property. + * @return {Object|Boolean} The representation of the declaration, true means + * the property is declared without the need for further analysis. + */ + function buildTypeAnnotationDeclarationTypes(annotation) { + switch (annotation.type) { + case 'GenericTypeAnnotation': + if (typeScope(annotation.id.name)) { + return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name)); } + return true; + case 'ObjectTypeAnnotation': var shapeTypeDefinition = { type: 'shape', children: {} }; - iterateProperties(argument.properties, function(childKey, childValue) { - shapeTypeDefinition.children[childKey] = buildReactDeclarationTypes(childValue); + iterateProperties(annotation.properties, function(childKey, childValue) { + shapeTypeDefinition.children[childKey] = buildTypeAnnotationDeclarationTypes(childValue); }); return shapeTypeDefinition; - case 'arrayOf': - case 'objectOf': - return { - type: 'object', - children: { - __ANY_KEY__: buildReactDeclarationTypes(argument) - } - }; - case 'oneOfType': - if ( - !argument.elements || - !argument.elements.length - ) { - // Invalid proptype or cannot analyse statically - return true; - } + case 'UnionTypeAnnotation': var unionTypeDefinition = { type: 'union', children: [] }; - for (var i = 0, j = argument.elements.length; i < j; i++) { - var type = buildReactDeclarationTypes(argument.elements[i]); + for (var i = 0, j = annotation.types.length; i < j; i++) { + var type = buildTypeAnnotationDeclarationTypes(annotation.types[i]); // keep only complex type if (type !== true) { if (type.children === true) { @@ -367,543 +451,464 @@ module.exports = Components.detect(function(context, components, utils) { unionTypeDefinition.children.push(type); } - if (unionTypeDefinition.length === 0) { + if (unionTypeDefinition.children.length === 0) { // no complex type found, simply accept everything return true; } return unionTypeDefinition; - case 'instanceOf': + case 'ArrayTypeAnnotation': return { - type: 'instance', - // Accept all children because we can't know what type they are - children: true + type: 'object', + children: { + __ANY_KEY__: buildTypeAnnotationDeclarationTypes(annotation.elementType) + } }; - case 'oneOf': default: + // Unknown or accepts everything. return true; } } - // Unknown property or accepts everything (any, object, ...) - return true; - } - - /** - * Creates the representation of the React props type annotation for the component. - * The representation is used to verify nested used properties. - * @param {ASTNode} annotation Type annotation for the props class property. - * @return {Object|Boolean} The representation of the declaration, true means - * the property is declared without the need for further analysis. - */ - function buildTypeAnnotationDeclarationTypes(annotation) { - switch (annotation.type) { - case 'GenericTypeAnnotation': - if (typeScope(annotation.id.name)) { - return buildTypeAnnotationDeclarationTypes(typeScope(annotation.id.name)); - } - return true; - case 'ObjectTypeAnnotation': - var shapeTypeDefinition = { - type: 'shape', - children: {} - }; - iterateProperties(annotation.properties, function(childKey, childValue) { - shapeTypeDefinition.children[childKey] = buildTypeAnnotationDeclarationTypes(childValue); - }); - return shapeTypeDefinition; - case 'UnionTypeAnnotation': - var unionTypeDefinition = { - type: 'union', - children: [] - }; - for (var i = 0, j = annotation.types.length; i < j; i++) { - var type = buildTypeAnnotationDeclarationTypes(annotation.types[i]); - // keep only complex type - if (type !== true) { - if (type.children === true) { - // every child is accepted for one type, abort type analysis - unionTypeDefinition.children = true; - return unionTypeDefinition; - } - } - unionTypeDefinition.children.push(type); - } - if (unionTypeDefinition.children.length === 0) { - // no complex type found, simply accept everything + /** + * Check if we are in a class constructor + * @return {boolean} true if we are in a class constructor, false if not + */ + function inConstructor() { + var scope = context.getScope(); + while (scope) { + if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { return true; } - return unionTypeDefinition; - case 'ArrayTypeAnnotation': - return { - type: 'object', - children: { - __ANY_KEY__: buildTypeAnnotationDeclarationTypes(annotation.elementType) - } - }; - default: - // Unknown or accepts everything. - return true; - } - } - - /** - * Check if we are in a class constructor - * @return {boolean} true if we are in a class constructor, false if not - */ - function inConstructor() { - var scope = context.getScope(); - while (scope) { - if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') { - return true; + scope = scope.upper; } - scope = scope.upper; + return false; } - return false; - } - - /** - * Retrieve the name of a property node - * @param {ASTNode} node The AST node with the property. - * @return {string} the name of the property or undefined if not found - */ - function getPropertyName(node) { - var isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); - var isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component(); - var isNotInConstructor = !inConstructor(node); - if (isDirectProp && isInClassComponent && isNotInConstructor) { + + /** + * Retrieve the name of a property node + * @param {ASTNode} node The AST node with the property. + * @return {string} the name of the property or undefined if not found + */ + function getPropertyName(node) { + var isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); + var isInClassComponent = utils.getParentES6Component() || utils.getParentES5Component(); + var isNotInConstructor = !inConstructor(node); + if (isDirectProp && isInClassComponent && isNotInConstructor) { + return void 0; + } + if (!isDirectProp) { + node = node.parent; + } + var property = node.property; + if (property) { + switch (property.type) { + case 'Identifier': + if (node.computed) { + return '__COMPUTED_PROP__'; + } + return property.name; + case 'MemberExpression': + return void 0; + case 'Literal': + // Accept computed properties that are literal strings + if (typeof property.value === 'string') { + return property.value; + } + // falls through + default: + if (node.computed) { + return '__COMPUTED_PROP__'; + } + break; + } + } return void 0; } - if (!isDirectProp) { - node = node.parent; - } - var property = node.property; - if (property) { - switch (property.type) { - case 'Identifier': - if (node.computed) { - return '__COMPUTED_PROP__'; - } - return property.name; + + /** + * Mark a prop type as used + * @param {ASTNode} node The AST node being marked. + */ + function markPropTypesAsUsed(node, parentNames) { + parentNames = parentNames || []; + var type; + var name; + var allNames; + var properties; + switch (node.type) { case 'MemberExpression': - return void 0; - case 'Literal': - // Accept computed properties that are literal strings - if (typeof property.value === 'string') { - return property.value; - } - // falls through - default: - if (node.computed) { - return '__COMPUTED_PROP__'; + name = getPropertyName(node); + if (name) { + allNames = parentNames.concat(name); + if (node.parent.type === 'MemberExpression') { + markPropTypesAsUsed(node.parent, allNames); + } + // Do not mark computed props as used. + type = name !== '__COMPUTED_PROP__' ? 'direct' : null; + } else if ( + node.parent.id && + node.parent.id.properties && + node.parent.id.properties.length && + getKeyValue(node.parent.id.properties[0]) + ) { + type = 'destructuring'; + properties = node.parent.id.properties; } break; - } - } - return void 0; - } - - /** - * Mark a prop type as used - * @param {ASTNode} node The AST node being marked. - */ - function markPropTypesAsUsed(node, parentNames) { - parentNames = parentNames || []; - var type; - var name; - var allNames; - var properties; - switch (node.type) { - case 'MemberExpression': - name = getPropertyName(node); - if (name) { - allNames = parentNames.concat(name); - if (node.parent.type === 'MemberExpression') { - markPropTypesAsUsed(node.parent, allNames); - } - // Do not mark computed props as used. - type = name !== '__COMPUTED_PROP__' ? 'direct' : null; - } else if ( - node.parent.id && - node.parent.id.properties && - node.parent.id.properties.length && - getKeyValue(node.parent.id.properties[0]) - ) { + case 'ArrowFunctionExpression': + case 'FunctionDeclaration': + case 'FunctionExpression': type = 'destructuring'; - properties = node.parent.id.properties; - } - break; - case 'ArrowFunctionExpression': - case 'FunctionDeclaration': - case 'FunctionExpression': - type = 'destructuring'; - properties = node.params[0].properties; - break; - case 'VariableDeclarator': - for (var i = 0, j = node.id.properties.length; i < j; i++) { - // let {props: {firstname}} = this - var thisDestructuring = ( - !hasSpreadOperator(node.id.properties[i]) && - (node.id.properties[i].key.name === 'props' || node.id.properties[i].key.value === 'props') && - node.id.properties[i].value.type === 'ObjectPattern' - ); - // let {firstname} = props - var directDestructuring = - node.init.name === 'props' && - (utils.getParentStatelessComponent() || inConstructor()) - ; - - if (thisDestructuring) { - properties = node.id.properties[i].value.properties; - } else if (directDestructuring) { - properties = node.id.properties; - } else { - continue; + properties = node.params[0].properties; + break; + case 'VariableDeclarator': + for (var i = 0, j = node.id.properties.length; i < j; i++) { + // let {props: {firstname}} = this + var thisDestructuring = ( + !hasSpreadOperator(node.id.properties[i]) && + (node.id.properties[i].key.name === 'props' || node.id.properties[i].key.value === 'props') && + node.id.properties[i].value.type === 'ObjectPattern' + ); + // let {firstname} = props + var directDestructuring = + node.init.name === 'props' && + (utils.getParentStatelessComponent() || inConstructor()) + ; + + if (thisDestructuring) { + properties = node.id.properties[i].value.properties; + } else if (directDestructuring) { + properties = node.id.properties; + } else { + continue; + } + type = 'destructuring'; + break; } - type = 'destructuring'; break; - } - break; - default: - throw new Error(node.type + ' ASTNodes are not handled by markPropTypesAsUsed'); - } + default: + throw new Error(node.type + ' ASTNodes are not handled by markPropTypesAsUsed'); + } - var component = components.get(utils.getParentComponent()); - var usedPropTypes = component && component.usedPropTypes || []; + var component = components.get(utils.getParentComponent()); + var usedPropTypes = component && component.usedPropTypes || []; - switch (type) { - case 'direct': - // Ignore Object methods - if (Object.prototype[name]) { - break; - } + switch (type) { + case 'direct': + // Ignore Object methods + if (Object.prototype[name]) { + break; + } - var isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); + var isDirectProp = DIRECT_PROPS_REGEX.test(sourceCode.getText(node)); - usedPropTypes.push({ - name: name, - allNames: allNames, - node: !isDirectProp && !inConstructor(node) ? node.parent.property : node.property - }); - break; - case 'destructuring': - for (var k = 0, l = properties.length; k < l; k++) { - if (hasSpreadOperator(properties[k]) || properties[k].computed) { - continue; - } - var propName = getKeyValue(properties[k]); + usedPropTypes.push({ + name: name, + allNames: allNames, + node: !isDirectProp && !inConstructor(node) ? node.parent.property : node.property + }); + break; + case 'destructuring': + for (var k = 0, l = properties.length; k < l; k++) { + if (hasSpreadOperator(properties[k]) || properties[k].computed) { + continue; + } + var propName = getKeyValue(properties[k]); - var currentNode = node; - allNames = []; - while (currentNode.property && currentNode.property.name !== 'props') { - allNames.unshift(currentNode.property.name); - currentNode = currentNode.object; + var currentNode = node; + allNames = []; + while (currentNode.property && currentNode.property.name !== 'props') { + allNames.unshift(currentNode.property.name); + currentNode = currentNode.object; + } + allNames.push(propName); + + if (propName) { + usedPropTypes.push({ + name: propName, + allNames: allNames, + node: properties[k] + }); + } } - allNames.push(propName); + break; + default: + break; + } - if (propName) { - usedPropTypes.push({ - name: propName, - allNames: allNames, - node: properties[k] - }); - } - } - break; - default: - break; + components.set(node, { + usedPropTypes: usedPropTypes + }); } - components.set(node, { - usedPropTypes: usedPropTypes - }); - } - - /** - * Mark a prop type as declared - * @param {ASTNode} node The AST node being checked. - * @param {propTypes} node The AST node containing the proptypes - */ - function markPropTypesAsDeclared(node, propTypes) { - var component = components.get(node); - var declaredPropTypes = component && component.declaredPropTypes || {}; - var ignorePropsValidation = false; - - switch (propTypes && propTypes.type) { - case 'ObjectTypeAnnotation': - iterateProperties(propTypes.properties, function(key, value) { - declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value); - }); - break; - case 'ObjectExpression': - iterateProperties(propTypes.properties, function(key, value) { - if (!value) { - ignorePropsValidation = true; - return; + /** + * Mark a prop type as declared + * @param {ASTNode} node The AST node being checked. + * @param {propTypes} node The AST node containing the proptypes + */ + function markPropTypesAsDeclared(node, propTypes) { + var component = components.get(node); + var declaredPropTypes = component && component.declaredPropTypes || {}; + var ignorePropsValidation = false; + + switch (propTypes && propTypes.type) { + case 'ObjectTypeAnnotation': + iterateProperties(propTypes.properties, function(key, value) { + declaredPropTypes[key] = buildTypeAnnotationDeclarationTypes(value); + }); + break; + case 'ObjectExpression': + iterateProperties(propTypes.properties, function(key, value) { + if (!value) { + ignorePropsValidation = true; + return; + } + declaredPropTypes[key] = buildReactDeclarationTypes(value); + }); + break; + case 'MemberExpression': + var curDeclaredPropTypes = declaredPropTypes; + // Walk the list of properties, until we reach the assignment + // ie: ClassX.propTypes.a.b.c = ... + while ( + propTypes && + propTypes.parent && + propTypes.parent.type !== 'AssignmentExpression' && + propTypes.property && + curDeclaredPropTypes + ) { + var propName = propTypes.property.name; + if (propName in curDeclaredPropTypes) { + curDeclaredPropTypes = curDeclaredPropTypes[propName].children; + propTypes = propTypes.parent; + } else { + // This will crash at runtime because we haven't seen this key before + // stop this and do not declare it + propTypes = null; + } } - declaredPropTypes[key] = buildReactDeclarationTypes(value); - }); - break; - case 'MemberExpression': - var curDeclaredPropTypes = declaredPropTypes; - // Walk the list of properties, until we reach the assignment - // ie: ClassX.propTypes.a.b.c = ... - while ( - propTypes && - propTypes.parent && - propTypes.parent.type !== 'AssignmentExpression' && - propTypes.property && - curDeclaredPropTypes - ) { - var propName = propTypes.property.name; - if (propName in curDeclaredPropTypes) { - curDeclaredPropTypes = curDeclaredPropTypes[propName].children; - propTypes = propTypes.parent; + if (propTypes && propTypes.parent && propTypes.property) { + curDeclaredPropTypes[propTypes.property.name] = + buildReactDeclarationTypes(propTypes.parent.right); } else { - // This will crash at runtime because we haven't seen this key before - // stop this and do not declare it - propTypes = null; + ignorePropsValidation = true; } - } - if (propTypes && propTypes.parent && propTypes.property) { - curDeclaredPropTypes[propTypes.property.name] = - buildReactDeclarationTypes(propTypes.parent.right); - } else { + break; + case 'Identifier': + var variablesInScope = variable.variablesInScope(context); + for (var i = 0, j = variablesInScope.length; i < j; i++) { + if (variablesInScope[i].name !== propTypes.name) { + continue; + } + var defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1]; + markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init); + return; + } + ignorePropsValidation = true; + break; + case null: + break; + default: ignorePropsValidation = true; + break; + } + + components.set(node, { + declaredPropTypes: declaredPropTypes, + ignorePropsValidation: ignorePropsValidation + }); + } + + /** + * Reports undeclared proptypes for a given component + * @param {Object} component The component to process + */ + function reportUndeclaredPropTypes(component) { + var allNames; + for (var i = 0, j = component.usedPropTypes.length; i < j; i++) { + allNames = component.usedPropTypes[i].allNames; + if ( + isIgnored(allNames[0]) || + isDeclaredInComponent(component.node, allNames) + ) { + continue; } - break; - case 'Identifier': - var variablesInScope = variable.variablesInScope(context); - for (var i = 0, j = variablesInScope.length; i < j; i++) { - if (variablesInScope[i].name !== propTypes.name) { - continue; + context.report( + component.usedPropTypes[i].node, + MISSING_MESSAGE, { + name: allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]') } - var defInScope = variablesInScope[i].defs[variablesInScope[i].defs.length - 1]; - markPropTypesAsDeclared(node, defInScope.node && defInScope.node.init); - return; - } - ignorePropsValidation = true; - break; - case null: - break; - default: - ignorePropsValidation = true; - break; + ); + } } - components.set(node, { - declaredPropTypes: declaredPropTypes, - ignorePropsValidation: ignorePropsValidation - }); - } - - /** - * Reports undeclared proptypes for a given component - * @param {Object} component The component to process - */ - function reportUndeclaredPropTypes(component) { - var allNames; - for (var i = 0, j = component.usedPropTypes.length; i < j; i++) { - allNames = component.usedPropTypes[i].allNames; - if ( - isIgnored(allNames[0]) || - isDeclaredInComponent(component.node, allNames) - ) { - continue; + /** + * Resolve the type annotation for a given node. + * Flow annotations are sometimes wrapped in outer `TypeAnnotation` + * and `NullableTypeAnnotation` nodes which obscure the annotation we're + * interested in. + * This method also resolves type aliases where possible. + * + * @param {ASTNode} node The annotation or a node containing the type annotation. + * @returns {ASTNode} The resolved type annotation for the node. + */ + function resolveTypeAnnotation(node) { + var annotation = node.typeAnnotation || node; + while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) { + annotation = annotation.typeAnnotation; } - context.report( - component.usedPropTypes[i].node, - MISSING_MESSAGE, { - name: allNames.join('.').replace(/\.__COMPUTED_PROP__/g, '[]') - } - ); - } - } - - /** - * Resolve the type annotation for a given node. - * Flow annotations are sometimes wrapped in outer `TypeAnnotation` - * and `NullableTypeAnnotation` nodes which obscure the annotation we're - * interested in. - * This method also resolves type aliases where possible. - * - * @param {ASTNode} node The annotation or a node containing the type annotation. - * @returns {ASTNode} The resolved type annotation for the node. - */ - function resolveTypeAnnotation(node) { - var annotation = node.typeAnnotation || node; - while (annotation && (annotation.type === 'TypeAnnotation' || annotation.type === 'NullableTypeAnnotation')) { - annotation = annotation.typeAnnotation; - } - if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) { - return typeScope(annotation.id.name); - } - return annotation; - } - - /** - * @param {ASTNode} node We expect either an ArrowFunctionExpression, - * FunctionDeclaration, or FunctionExpression - */ - function markDestructuredFunctionArgumentsAsUsed(node) { - var destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern'; - if (destructuring && components.get(node)) { - markPropTypesAsUsed(node); - } - } - - /** - * @param {ASTNode} node We expect either an ArrowFunctionExpression, - * FunctionDeclaration, or FunctionExpression - */ - function markAnnotatedFunctionArgumentsAsDeclared(node) { - if (!node.params || !node.params.length || !isAnnotatedFunctionPropsDeclaration(node)) { - return; + if (annotation.type === 'GenericTypeAnnotation' && typeScope(annotation.id.name)) { + return typeScope(annotation.id.name); + } + return annotation; } - markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0])); - } - - /** - * @param {ASTNode} node We expect either an ArrowFunctionExpression, - * FunctionDeclaration, or FunctionExpression - */ - function handleStatelessComponent(node) { - markDestructuredFunctionArgumentsAsUsed(node); - markAnnotatedFunctionArgumentsAsDeclared(node); - } - - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- - - return { - ClassProperty: function(node) { - if (isAnnotatedClassPropsDeclaration(node)) { - markPropTypesAsDeclared(node, resolveTypeAnnotation(node)); - } else if (isPropTypesDeclaration(node)) { - markPropTypesAsDeclared(node, node.value); + + /** + * @param {ASTNode} node We expect either an ArrowFunctionExpression, + * FunctionDeclaration, or FunctionExpression + */ + function markDestructuredFunctionArgumentsAsUsed(node) { + var destructuring = node.params && node.params[0] && node.params[0].type === 'ObjectPattern'; + if (destructuring && components.get(node)) { + markPropTypesAsUsed(node); } - }, - - VariableDeclarator: function(node) { - var destructuring = node.init && node.id && node.id.type === 'ObjectPattern'; - // let {props: {firstname}} = this - var thisDestructuring = destructuring && node.init.type === 'ThisExpression'; - // let {firstname} = props - var directDestructuring = - destructuring && - node.init.name === 'props' && - (utils.getParentStatelessComponent() || inConstructor()) - ; - - if (!thisDestructuring && !directDestructuring) { + } + + /** + * @param {ASTNode} node We expect either an ArrowFunctionExpression, + * FunctionDeclaration, or FunctionExpression + */ + function markAnnotatedFunctionArgumentsAsDeclared(node) { + if (!node.params || !node.params.length || !isAnnotatedFunctionPropsDeclaration(node)) { return; } - markPropTypesAsUsed(node); - }, + markPropTypesAsDeclared(node, resolveTypeAnnotation(node.params[0])); + } - FunctionDeclaration: handleStatelessComponent, + /** + * @param {ASTNode} node We expect either an ArrowFunctionExpression, + * FunctionDeclaration, or FunctionExpression + */ + function handleStatelessComponent(node) { + markDestructuredFunctionArgumentsAsUsed(node); + markAnnotatedFunctionArgumentsAsDeclared(node); + } - ArrowFunctionExpression: handleStatelessComponent, + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - FunctionExpression: handleStatelessComponent, + return { + ClassProperty: function(node) { + if (isAnnotatedClassPropsDeclaration(node)) { + markPropTypesAsDeclared(node, resolveTypeAnnotation(node)); + } else if (isPropTypesDeclaration(node)) { + markPropTypesAsDeclared(node, node.value); + } + }, + + VariableDeclarator: function(node) { + var destructuring = node.init && node.id && node.id.type === 'ObjectPattern'; + // let {props: {firstname}} = this + var thisDestructuring = destructuring && node.init.type === 'ThisExpression'; + // let {firstname} = props + var directDestructuring = + destructuring && + node.init.name === 'props' && + (utils.getParentStatelessComponent() || inConstructor()) + ; + + if (!thisDestructuring && !directDestructuring) { + return; + } + markPropTypesAsUsed(node); + }, - MemberExpression: function(node) { - var type; - if (isPropTypesUsage(node)) { - type = 'usage'; - } else if (isPropTypesDeclaration(node.property)) { - type = 'declaration'; - } + FunctionDeclaration: handleStatelessComponent, - switch (type) { - case 'usage': - markPropTypesAsUsed(node); - break; - case 'declaration': - var component = utils.getRelatedComponent(node); - if (!component) { - return; - } - markPropTypesAsDeclared(component.node, node.parent.right || node.parent); - break; - default: - break; - } - }, + ArrowFunctionExpression: handleStatelessComponent, - MethodDefinition: function(node) { - if (!isPropTypesDeclaration(node.key)) { - return; - } + FunctionExpression: handleStatelessComponent, - var i = node.value.body.body.length - 1; - for (; i >= 0; i--) { - if (node.value.body.body[i].type === 'ReturnStatement') { - break; + MemberExpression: function(node) { + var type; + if (isPropTypesUsage(node)) { + type = 'usage'; + } else if (isPropTypesDeclaration(node.property)) { + type = 'declaration'; } - } - if (i >= 0) { - markPropTypesAsDeclared(node, node.value.body.body[i].argument); - } - }, + switch (type) { + case 'usage': + markPropTypesAsUsed(node); + break; + case 'declaration': + var component = utils.getRelatedComponent(node); + if (!component) { + return; + } + markPropTypesAsDeclared(component.node, node.parent.right || node.parent); + break; + default: + break; + } + }, - ObjectExpression: function(node) { - // Search for the proptypes declaration - node.properties.forEach(function(property) { - if (!isPropTypesDeclaration(property.key)) { + MethodDefinition: function(node) { + if (!isPropTypesDeclaration(node.key)) { return; } - markPropTypesAsDeclared(node, property.value); - }); - }, - - TypeAlias: function(node) { - typeScope(node.id.name, node.right); - }, - - Program: function() { - stack = [{}]; - }, - - BlockStatement: function () { - stack.push(Object.create(typeScope())); - }, - - 'BlockStatement:exit': function () { - stack.pop(); - }, - - 'Program:exit': function() { - stack = null; - var list = components.list(); - // Report undeclared proptypes for all classes - for (var component in list) { - if (!list.hasOwnProperty(component) || !mustBeValidated(list[component])) { - continue; + + var i = node.value.body.body.length - 1; + for (; i >= 0; i--) { + if (node.value.body.body[i].type === 'ReturnStatement') { + break; + } } - reportUndeclaredPropTypes(list[component]); - } - } - }; -}); + if (i >= 0) { + markPropTypesAsDeclared(node, node.value.body.body[i].argument); + } + }, -module.exports.schema = [{ - type: 'object', - properties: { - ignore: { - type: 'array', - items: { - type: 'string' - } - }, - customValidators: { - type: 'array', - items: { - type: 'string' + ObjectExpression: function(node) { + // Search for the proptypes declaration + node.properties.forEach(function(property) { + if (!isPropTypesDeclaration(property.key)) { + return; + } + markPropTypesAsDeclared(node, property.value); + }); + }, + + TypeAlias: function(node) { + typeScope(node.id.name, node.right); + }, + + Program: function() { + stack = [{}]; + }, + + BlockStatement: function () { + stack.push(Object.create(typeScope())); + }, + + 'BlockStatement:exit': function () { + stack.pop(); + }, + + 'Program:exit': function() { + stack = null; + var list = components.list(); + // Report undeclared proptypes for all classes + for (var component in list) { + if (!list.hasOwnProperty(component) || !mustBeValidated(list[component])) { + continue; + } + reportUndeclaredPropTypes(list[component]); + } } - } - }, - additionalProperties: false -}]; + }; + + }) +}; diff --git a/lib/rules/react-in-jsx-scope.js b/lib/rules/react-in-jsx-scope.js index 415c1fea8c..925e3b9dd0 100644 --- a/lib/rules/react-in-jsx-scope.js +++ b/lib/rules/react-in-jsx-scope.js @@ -11,33 +11,38 @@ var pragmaUtil = require('../util/pragma'); // Rule Definition // ----------------------------------------------------------------------------- -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + schema: [] + }, - var pragma = pragmaUtil.getFromContext(context); - var NOT_DEFINED_MESSAGE = '\'{{name}}\' must be in scope when using JSX'; + create: function(context) { - return { + var pragma = pragmaUtil.getFromContext(context); + var NOT_DEFINED_MESSAGE = '\'{{name}}\' must be in scope when using JSX'; - JSXOpeningElement: function(node) { - var variables = variableUtil.variablesInScope(context); - if (variableUtil.findVariable(variables, pragma)) { - return; - } - context.report({ - node: node, - message: NOT_DEFINED_MESSAGE, - data: { - name: pragma - } - }); - }, + return { - BlockComment: function(node) { - pragma = pragmaUtil.getFromNode(node) || pragma; - } + JSXOpeningElement: function(node) { + var variables = variableUtil.variablesInScope(context); + if (variableUtil.findVariable(variables, pragma)) { + return; + } + context.report({ + node: node, + message: NOT_DEFINED_MESSAGE, + data: { + name: pragma + } + }); + }, + + BlockComment: function(node) { + pragma = pragmaUtil.getFromNode(node) || pragma; + } - }; + }; + } }; - -module.exports.schema = []; diff --git a/lib/rules/require-extension.js b/lib/rules/require-extension.js index 8e25c665ae..27140b44ad 100644 --- a/lib/rules/require-extension.js +++ b/lib/rules/require-extension.js @@ -13,31 +13,37 @@ var util = require('util'); var jsxRequireExtension = require('./jsx-require-extension'); var isWarnedForDeprecation = false; -module.exports = function(context) { - return util._extend(jsxRequireExtension(context), { - Program: function() { - if (isWarnedForDeprecation || /\=-(f|-format)=/.test(process.argv.join('='))) { - return; - } +module.exports = { + meta: { + docs: {}, - /* eslint-disable no-console */ - console.log('The react/require-extension rule is deprecated. Please ' + - 'use the react/jsx-require-extension rule instead.'); - /* eslint-enable no-console */ - isWarnedForDeprecation = true; - } - }); -}; + schema: [{ + type: 'object', + properties: { + extensions: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false + }] + }, -module.exports.schema = [{ - type: 'object', - properties: { - extensions: { - type: 'array', - items: { - type: 'string' + create: function(context) { + return util._extend(jsxRequireExtension(context), { + Program: function() { + if (isWarnedForDeprecation || /\=-(f|-format)=/.test(process.argv.join('='))) { + return; + } + + /* eslint-disable no-console */ + console.log('The react/require-extension rule is deprecated. Please ' + + 'use the react/jsx-require-extension rule instead.'); + /* eslint-enable no-console */ + isWarnedForDeprecation = true; } - } - }, - additionalProperties: false -}]; + }); + } +}; diff --git a/lib/rules/require-optimization.js b/lib/rules/require-optimization.js index 9f330dd203..758c26f1ce 100644 --- a/lib/rules/require-optimization.js +++ b/lib/rules/require-optimization.js @@ -6,189 +6,195 @@ var Components = require('../util/Components'); -module.exports = Components.detect(function (context, components) { - var MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.'; - var configuration = context.options[0] || {}; - var allowDecorators = configuration.allowDecorators || []; - - /** - * Checks to see if our component is decorated by PureRenderMixin via reactMixin - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not. - */ - var hasPureRenderDecorator = function (node) { - if (node.decorators && node.decorators.length) { - for (var i = 0, l = node.decorators.length; i < l; i++) { - if ( - node.decorators[i].expression && - node.decorators[i].expression.callee && - node.decorators[i].expression.callee.object && - node.decorators[i].expression.callee.object.name === 'reactMixin' && - node.decorators[i].expression.callee.property && - node.decorators[i].expression.callee.property.name === 'decorate' && - node.decorators[i].expression.arguments && - node.decorators[i].expression.arguments.length && - node.decorators[i].expression.arguments[0].name === 'PureRenderMixin' - ) { - return true; +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: { + allowDecorators: { + type: 'array', + items: { + type: 'string' + } } - } - } - - return false; - }; - - /** - * Checks to see if our component is custom decorated - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if node is decorated name with a custom decorated, false if not. - */ - var hasCustomDecorator = function (node) { - var allowLenght = allowDecorators.length; - - if (allowLenght && node.decorators && node.decorators.length) { - for (var i = 0; i < allowLenght; i++) { - for (var j = 0, l = node.decorators.length; j < l; j++) { + }, + additionalProperties: false + }] + }, + + create: Components.detect(function (context, components) { + var MISSING_MESSAGE = 'Component is not optimized. Please add a shouldComponentUpdate method.'; + var configuration = context.options[0] || {}; + var allowDecorators = configuration.allowDecorators || []; + + /** + * Checks to see if our component is decorated by PureRenderMixin via reactMixin + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not. + */ + var hasPureRenderDecorator = function (node) { + if (node.decorators && node.decorators.length) { + for (var i = 0, l = node.decorators.length; i < l; i++) { if ( - node.decorators[j].expression && - node.decorators[j].expression.name === allowDecorators[i] + node.decorators[i].expression && + node.decorators[i].expression.callee && + node.decorators[i].expression.callee.object && + node.decorators[i].expression.callee.object.name === 'reactMixin' && + node.decorators[i].expression.callee.property && + node.decorators[i].expression.callee.property.name === 'decorate' && + node.decorators[i].expression.arguments && + node.decorators[i].expression.arguments.length && + node.decorators[i].expression.arguments[0].name === 'PureRenderMixin' ) { return true; } } } - } - - return false; - }; - - /** - * Checks if we are declaring a shouldComponentUpdate method - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not. - */ - var isSCUDeclarеd = function (node) { - return Boolean( - node && - node.name === 'shouldComponentUpdate' - ); - }; - - /** - * Checks if we are declaring a PureRenderMixin mixin - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not. - */ - var isPureRenderDeclared = function (node) { - var hasPR = false; - if (node.value && node.value.elements) { - for (var i = 0, l = node.value.elements.length; i < l; i++) { - if (node.value.elements[i].name === 'PureRenderMixin') { - hasPR = true; - break; + + return false; + }; + + /** + * Checks to see if our component is custom decorated + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if node is decorated name with a custom decorated, false if not. + */ + var hasCustomDecorator = function (node) { + var allowLenght = allowDecorators.length; + + if (allowLenght && node.decorators && node.decorators.length) { + for (var i = 0; i < allowLenght; i++) { + for (var j = 0, l = node.decorators.length; j < l; j++) { + if ( + node.decorators[j].expression && + node.decorators[j].expression.name === allowDecorators[i] + ) { + return true; + } + } } } - } - return Boolean( + return false; + }; + + /** + * Checks if we are declaring a shouldComponentUpdate method + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not. + */ + var isSCUDeclarеd = function (node) { + return Boolean( node && - node.key.name === 'mixins' && - hasPR + node.name === 'shouldComponentUpdate' ); - }; - - /** - * Mark shouldComponentUpdate as declared - * @param {ASTNode} node The AST node being checked. - */ - var markSCUAsDeclared = function (node) { - components.set(node, { - hasSCU: true - }); - }; - - /** - * Reports missing optimization for a given component - * @param {Object} component The component to process - */ - var reportMissingOptimization = function (component) { - context.report({ - node: component.node, - message: MISSING_MESSAGE, - data: { - component: component.name - } - }); - }; - - return { - ArrowFunctionExpression: function (node) { - // Stateless Functional Components cannot be optimized (yet) - markSCUAsDeclared(node); - }, - - ClassDeclaration: function (node) { - if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node))) { - return; - } - markSCUAsDeclared(node); - }, - - FunctionDeclaration: function (node) { - // Stateless Functional Components cannot be optimized (yet) - markSCUAsDeclared(node); - }, - - FunctionExpression: function (node) { - // Stateless Functional Components cannot be optimized (yet) - markSCUAsDeclared(node); - }, - - MethodDefinition: function (node) { - if (!isSCUDeclarеd(node.key)) { - return; + }; + + /** + * Checks if we are declaring a PureRenderMixin mixin + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not. + */ + var isPureRenderDeclared = function (node) { + var hasPR = false; + if (node.value && node.value.elements) { + for (var i = 0, l = node.value.elements.length; i < l; i++) { + if (node.value.elements[i].name === 'PureRenderMixin') { + hasPR = true; + break; + } + } } - markSCUAsDeclared(node); - }, - - ObjectExpression: function (node) { - // Search for the shouldComponentUpdate declaration - for (var i = 0, l = node.properties.length; i < l; i++) { - if ( - !node.properties[i].key || ( - !isSCUDeclarеd(node.properties[i].key) && - !isPureRenderDeclared(node.properties[i]) - ) - ) { - continue; + + return Boolean( + node && + node.key.name === 'mixins' && + hasPR + ); + }; + + /** + * Mark shouldComponentUpdate as declared + * @param {ASTNode} node The AST node being checked. + */ + var markSCUAsDeclared = function (node) { + components.set(node, { + hasSCU: true + }); + }; + + /** + * Reports missing optimization for a given component + * @param {Object} component The component to process + */ + var reportMissingOptimization = function (component) { + context.report({ + node: component.node, + message: MISSING_MESSAGE, + data: { + component: component.name } + }); + }; + + return { + ArrowFunctionExpression: function (node) { + // Stateless Functional Components cannot be optimized (yet) markSCUAsDeclared(node); - } - }, + }, + + ClassDeclaration: function (node) { + if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node))) { + return; + } + markSCUAsDeclared(node); + }, + + FunctionDeclaration: function (node) { + // Stateless Functional Components cannot be optimized (yet) + markSCUAsDeclared(node); + }, - 'Program:exit': function () { - var list = components.list(); + FunctionExpression: function (node) { + // Stateless Functional Components cannot be optimized (yet) + markSCUAsDeclared(node); + }, - // Report missing shouldComponentUpdate for all components - for (var component in list) { - if (!list.hasOwnProperty(component) || list[component].hasSCU) { - continue; + MethodDefinition: function (node) { + if (!isSCUDeclarеd(node.key)) { + return; + } + markSCUAsDeclared(node); + }, + + ObjectExpression: function (node) { + // Search for the shouldComponentUpdate declaration + for (var i = 0, l = node.properties.length; i < l; i++) { + if ( + !node.properties[i].key || ( + !isSCUDeclarеd(node.properties[i].key) && + !isPureRenderDeclared(node.properties[i]) + ) + ) { + continue; + } + markSCUAsDeclared(node); + } + }, + + 'Program:exit': function () { + var list = components.list(); + + // Report missing shouldComponentUpdate for all components + for (var component in list) { + if (!list.hasOwnProperty(component) || list[component].hasSCU) { + continue; + } + reportMissingOptimization(list[component]); } - reportMissingOptimization(list[component]); - } - } - }; -}); - -module.exports.schema = [{ - type: 'object', - properties: { - allowDecorators: { - type: 'array', - items: { - type: 'string' } - } - }, - additionalProperties: false -}]; + }; + }) +}; diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js index ccec8c6070..cc39a8b7ef 100644 --- a/lib/rules/require-render-return.js +++ b/lib/rules/require-render-return.js @@ -10,111 +10,116 @@ var Components = require('../util/Components'); // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components, utils) { +module.exports = { + meta: { + docs: {}, + schema: [{}] + }, - /** - * Mark a return statement as present - * @param {ASTNode} node The AST node being checked. - */ - function markReturnStatementPresent(node) { - components.set(node, { - hasReturnStatement: true - }); - } + create: Components.detect(function(context, components, utils) { - /** - * Get properties for a given AST node - * @param {ASTNode} node The AST node being checked. - * @returns {Array} Properties array. - */ - function getComponentProperties(node) { - switch (node.type) { - case 'ClassDeclaration': - return node.body.body; - case 'ObjectExpression': - return node.properties; - default: - return []; + /** + * Mark a return statement as present + * @param {ASTNode} node The AST node being checked. + */ + function markReturnStatementPresent(node) { + components.set(node, { + hasReturnStatement: true + }); } - } - /** - * Get properties name - * @param {Object} node - Property. - * @returns {String} Property name. - */ - function getPropertyName(node) { - // Special case for class properties - // (babel-eslint does not expose property name so we have to rely on tokens) - if (node.type === 'ClassProperty') { - var tokens = context.getFirstTokens(node, 2); - return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; - } else if (['MethodDefinition', 'Property'].indexOf(node.type) !== -1) { - return node.key.name; + /** + * Get properties for a given AST node + * @param {ASTNode} node The AST node being checked. + * @returns {Array} Properties array. + */ + function getComponentProperties(node) { + switch (node.type) { + case 'ClassDeclaration': + return node.body.body; + case 'ObjectExpression': + return node.properties; + default: + return []; + } } - return ''; - } - /** - * Check if a given AST node has a render method - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if there is a render method, false if not - */ - function hasRenderMethod(node) { - var properties = getComponentProperties(node); - for (var i = 0, j = properties.length; i < j; i++) { - if (getPropertyName(properties[i]) !== 'render') { - continue; + /** + * Get properties name + * @param {Object} node - Property. + * @returns {String} Property name. + */ + function getPropertyName(node) { + // Special case for class properties + // (babel-eslint does not expose property name so we have to rely on tokens) + if (node.type === 'ClassProperty') { + var tokens = context.getFirstTokens(node, 2); + return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; + } else if (['MethodDefinition', 'Property'].indexOf(node.type) !== -1) { + return node.key.name; } - return /FunctionExpression$/.test(properties[i].value.type); + return ''; } - return false; - } - return { - ReturnStatement: function(node) { - var ancestors = context.getAncestors(node).reverse(); - var depth = 0; - for (var i = 0, j = ancestors.length; i < j; i++) { - if (/Function(Expression|Declaration)$/.test(ancestors[i].type)) { - depth++; - } - if ( - !/(MethodDefinition|(Class)?Property)$/.test(ancestors[i].type) || - getPropertyName(ancestors[i]) !== 'render' || - depth > 1 - ) { + /** + * Check if a given AST node has a render method + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if there is a render method, false if not + */ + function hasRenderMethod(node) { + var properties = getComponentProperties(node); + for (var i = 0, j = properties.length; i < j; i++) { + if (getPropertyName(properties[i]) !== 'render') { continue; } - markReturnStatementPresent(node); + return /FunctionExpression$/.test(properties[i].value.type); } - }, + return false; + } - ArrowFunctionExpression: function(node) { - if (node.expression === false || getPropertyName(node.parent) !== 'render') { - return; - } - markReturnStatementPresent(node); - }, + return { + ReturnStatement: function(node) { + var ancestors = context.getAncestors(node).reverse(); + var depth = 0; + for (var i = 0, j = ancestors.length; i < j; i++) { + if (/Function(Expression|Declaration)$/.test(ancestors[i].type)) { + depth++; + } + if ( + !/(MethodDefinition|(Class)?Property)$/.test(ancestors[i].type) || + getPropertyName(ancestors[i]) !== 'render' || + depth > 1 + ) { + continue; + } + markReturnStatementPresent(node); + } + }, - 'Program:exit': function() { - var list = components.list(); - for (var component in list) { - if ( - !list.hasOwnProperty(component) || - !hasRenderMethod(list[component].node) || - list[component].hasReturnStatement || - (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) - ) { - continue; + ArrowFunctionExpression: function(node) { + if (node.expression === false || getPropertyName(node.parent) !== 'render') { + return; } - context.report({ - node: list[component].node, - message: 'Your render method should have return statement' - }); - } - } - }; -}); + markReturnStatementPresent(node); + }, -module.exports.schema = [{}]; + 'Program:exit': function() { + var list = components.list(); + for (var component in list) { + if ( + !list.hasOwnProperty(component) || + !hasRenderMethod(list[component].node) || + list[component].hasReturnStatement || + (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node)) + ) { + continue; + } + context.report({ + node: list[component].node, + message: 'Your render method should have return statement' + }); + } + } + }; + }) +}; diff --git a/lib/rules/self-closing-comp.js b/lib/rules/self-closing-comp.js index 8e4f38c82c..2afc8ddc1f 100644 --- a/lib/rules/self-closing-comp.js +++ b/lib/rules/self-closing-comp.js @@ -8,67 +8,73 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, - var tagConvention = /^[a-z]|\-/; - function isTagName(name) { - return tagConvention.test(name); - } + schema: [{ + type: 'object', + properties: { + component: { + default: true, + type: 'boolean' + }, + html: { + default: false, + type: 'boolean' + } + }, + additionalProperties: false + }] + }, - function isComponent(node) { - return node.name && node.name.type === 'JSXIdentifier' && !isTagName(node.name.name); - } + create: function(context) { - function hasChildren(node) { - var childrens = node.parent.children; - if ( - !childrens.length || - (childrens.length === 1 && childrens[0].type === 'Literal' && !childrens[0].value.replace(/(?!\xA0)\s/g, '')) - ) { - return false; + var tagConvention = /^[a-z]|\-/; + function isTagName(name) { + return tagConvention.test(name); } - return true; - } - function isShouldBeSelfClosed(node) { - var configuration = context.options[0] || {component: true}; - return ( - configuration.component && isComponent(node) || - configuration.html && isTagName(node.name.name) - ) && !node.selfClosing && !hasChildren(node); - } + function isComponent(node) { + return node.name && node.name.type === 'JSXIdentifier' && !isTagName(node.name.name); + } + + function hasChildren(node) { + var childrens = node.parent.children; + if ( + !childrens.length || + (childrens.length === 1 && childrens[0].type === 'Literal' && !childrens[0].value.replace(/(?!\xA0)\s/g, '')) + ) { + return false; + } + return true; + } + + function isShouldBeSelfClosed(node) { + var configuration = context.options[0] || {component: true}; + return ( + configuration.component && isComponent(node) || + configuration.html && isTagName(node.name.name) + ) && !node.selfClosing && !hasChildren(node); + } - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- - return { + return { - JSXOpeningElement: function(node) { + JSXOpeningElement: function(node) { - if (!isShouldBeSelfClosed(node)) { - return; + if (!isShouldBeSelfClosed(node)) { + return; + } + context.report({ + node: node, + message: 'Empty components are self-closing' + }); } - context.report({ - node: node, - message: 'Empty components are self-closing' - }); - } - }; + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - component: { - default: true, - type: 'boolean' - }, - html: { - default: false, - type: 'boolean' - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/sort-comp.js b/lib/rules/sort-comp.js index a4d5e0d0af..cc53549cb8 100644 --- a/lib/rules/sort-comp.js +++ b/lib/rules/sort-comp.js @@ -38,366 +38,372 @@ function getMethodsOrder(defaultConfig, userConfig) { // Rule Definition // ------------------------------------------------------------------------------ -module.exports = Components.detect(function(context, components) { - - var errors = {}; - - var MISPOSITION_MESSAGE = '{{propA}} should be placed {{position}} {{propB}}'; - - var methodsOrder = getMethodsOrder({ - order: [ - 'static-methods', - 'lifecycle', - 'everything-else', - 'render' - ], - groups: { - lifecycle: [ - 'displayName', - 'propTypes', - 'contextTypes', - 'childContextTypes', - 'mixins', - 'statics', - 'defaultProps', - 'constructor', - 'getDefaultProps', - 'state', - 'getInitialState', - 'getChildContext', - 'componentWillMount', - 'componentDidMount', - 'componentWillReceiveProps', - 'shouldComponentUpdate', - 'componentWillUpdate', - 'componentDidUpdate', - 'componentWillUnmount' - ] - } - }, context.options[0]); - - // -------------------------------------------------------------------------- - // Public - // -------------------------------------------------------------------------- - - var regExpRegExp = /\/(.*)\/([g|y|i|m]*)/; - - /** - * Get indexes of the matching patterns in methods order configuration - * @param {Object} method - Method metadata. - * @returns {Array} The matching patterns indexes. Return [Infinity] if there is no match. - */ - function getRefPropIndexes(method) { - var isRegExp; - var matching; - var i; - var j; - var indexes = []; - - if (method.static) { - for (i = 0, j = methodsOrder.length; i < j; i++) { - if (methodsOrder[i] === 'static-methods') { - indexes.push(i); - break; - } - } - } +module.exports = { + meta: { + docs: {}, - // Either this is not a static method or static methods are not specified - // in the methodsOrder. - if (indexes.length === 0) { - for (i = 0, j = methodsOrder.length; i < j; i++) { - isRegExp = methodsOrder[i].match(regExpRegExp); - if (isRegExp) { - matching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name); - } else { - matching = methodsOrder[i] === method.name; + schema: [{ + type: 'object', + properties: { + order: { + type: 'array', + items: { + type: 'string' + } + }, + groups: { + type: 'object', + patternProperties: { + '^.*$': { + type: 'array', + items: { + type: 'string' + } + } + } } - if (matching) { - indexes.push(i); + }, + additionalProperties: false + }] + }, + + create: Components.detect(function(context, components) { + + var errors = {}; + + var MISPOSITION_MESSAGE = '{{propA}} should be placed {{position}} {{propB}}'; + + var methodsOrder = getMethodsOrder({ + order: [ + 'static-methods', + 'lifecycle', + 'everything-else', + 'render' + ], + groups: { + lifecycle: [ + 'displayName', + 'propTypes', + 'contextTypes', + 'childContextTypes', + 'mixins', + 'statics', + 'defaultProps', + 'constructor', + 'getDefaultProps', + 'state', + 'getInitialState', + 'getChildContext', + 'componentWillMount', + 'componentDidMount', + 'componentWillReceiveProps', + 'shouldComponentUpdate', + 'componentWillUpdate', + 'componentDidUpdate', + 'componentWillUnmount' + ] + } + }, context.options[0]); + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + var regExpRegExp = /\/(.*)\/([g|y|i|m]*)/; + + /** + * Get indexes of the matching patterns in methods order configuration + * @param {Object} method - Method metadata. + * @returns {Array} The matching patterns indexes. Return [Infinity] if there is no match. + */ + function getRefPropIndexes(method) { + var isRegExp; + var matching; + var i; + var j; + var indexes = []; + + if (method.static) { + for (i = 0, j = methodsOrder.length; i < j; i++) { + if (methodsOrder[i] === 'static-methods') { + indexes.push(i); + break; + } } } - } - // No matching pattern, return 'everything-else' index - if (indexes.length === 0) { - for (i = 0, j = methodsOrder.length; i < j; i++) { - if (methodsOrder[i] === 'everything-else') { - indexes.push(i); - break; + // Either this is not a static method or static methods are not specified + // in the methodsOrder. + if (indexes.length === 0) { + for (i = 0, j = methodsOrder.length; i < j; i++) { + isRegExp = methodsOrder[i].match(regExpRegExp); + if (isRegExp) { + matching = new RegExp(isRegExp[1], isRegExp[2]).test(method.name); + } else { + matching = methodsOrder[i] === method.name; + } + if (matching) { + indexes.push(i); + } } } - } - // No matching pattern and no 'everything-else' group - if (indexes.length === 0) { - indexes.push(Infinity); - } + // No matching pattern, return 'everything-else' index + if (indexes.length === 0) { + for (i = 0, j = methodsOrder.length; i < j; i++) { + if (methodsOrder[i] === 'everything-else') { + indexes.push(i); + break; + } + } + } - return indexes; - } + // No matching pattern and no 'everything-else' group + if (indexes.length === 0) { + indexes.push(Infinity); + } - /** - * Get properties name - * @param {Object} node - Property. - * @returns {String} Property name. - */ - function getPropertyName(node) { - // Special case for class properties - // (babel-eslint does not expose property name so we have to rely on tokens) - if (node.type === 'ClassProperty') { - var tokens = context.getFirstTokens(node, 2); - return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; + return indexes; } - return node.key.name; - } + /** + * Get properties name + * @param {Object} node - Property. + * @returns {String} Property name. + */ + function getPropertyName(node) { + // Special case for class properties + // (babel-eslint does not expose property name so we have to rely on tokens) + if (node.type === 'ClassProperty') { + var tokens = context.getFirstTokens(node, 2); + return tokens[1] && tokens[1].type === 'Identifier' ? tokens[1].value : tokens[0].value; + } - /** - * Store a new error in the error list - * @param {Object} propA - Mispositioned property. - * @param {Object} propB - Reference property. - */ - function storeError(propA, propB) { - // Initialize the error object if needed - if (!errors[propA.index]) { - errors[propA.index] = { - node: propA.node, - score: 0, - closest: { - distance: Infinity, - ref: { - node: null, - index: 0 - } - } - }; - } - // Increment the prop score - errors[propA.index].score++; - // Stop here if we already have a closer reference - if (Math.abs(propA.index - propB.index) > errors[propA.index].closest.distance) { - return; + return node.key.name; } - // Update the closest reference - errors[propA.index].closest.distance = Math.abs(propA.index - propB.index); - errors[propA.index].closest.ref.node = propB.node; - errors[propA.index].closest.ref.index = propB.index; - } - /** - * Dedupe errors, only keep the ones with the highest score and delete the others - */ - function dedupeErrors() { - for (var i in errors) { - if (!errors.hasOwnProperty(i)) { - continue; - } - var index = errors[i].closest.ref.index; - if (!errors[index]) { - continue; + /** + * Store a new error in the error list + * @param {Object} propA - Mispositioned property. + * @param {Object} propB - Reference property. + */ + function storeError(propA, propB) { + // Initialize the error object if needed + if (!errors[propA.index]) { + errors[propA.index] = { + node: propA.node, + score: 0, + closest: { + distance: Infinity, + ref: { + node: null, + index: 0 + } + } + }; } - if (errors[i].score > errors[index].score) { - delete errors[index]; - } else { - delete errors[i]; + // Increment the prop score + errors[propA.index].score++; + // Stop here if we already have a closer reference + if (Math.abs(propA.index - propB.index) > errors[propA.index].closest.distance) { + return; } + // Update the closest reference + errors[propA.index].closest.distance = Math.abs(propA.index - propB.index); + errors[propA.index].closest.ref.node = propB.node; + errors[propA.index].closest.ref.index = propB.index; } - } - /** - * Report errors - */ - function reportErrors() { - dedupeErrors(); - - var nodeA; - var nodeB; - var indexA; - var indexB; - for (var i in errors) { - if (!errors.hasOwnProperty(i)) { - continue; + /** + * Dedupe errors, only keep the ones with the highest score and delete the others + */ + function dedupeErrors() { + for (var i in errors) { + if (!errors.hasOwnProperty(i)) { + continue; + } + var index = errors[i].closest.ref.index; + if (!errors[index]) { + continue; + } + if (errors[i].score > errors[index].score) { + delete errors[index]; + } else { + delete errors[i]; + } } + } - nodeA = errors[i].node; - nodeB = errors[i].closest.ref.node; - indexA = i; - indexB = errors[i].closest.ref.index; - - context.report({ - node: nodeA, - message: MISPOSITION_MESSAGE, - data: { - propA: getPropertyName(nodeA), - propB: getPropertyName(nodeB), - position: indexA < indexB ? 'before' : 'after' + /** + * Report errors + */ + function reportErrors() { + dedupeErrors(); + + var nodeA; + var nodeB; + var indexA; + var indexB; + for (var i in errors) { + if (!errors.hasOwnProperty(i)) { + continue; } - }); - } - } - /** - * Get properties for a given AST node - * @param {ASTNode} node The AST node being checked. - * @returns {Array} Properties array. - */ - function getComponentProperties(node) { - switch (node.type) { - case 'ClassDeclaration': - return node.body.body; - case 'ObjectExpression': - return node.properties.filter(function(property) { - return property.type === 'Property'; + nodeA = errors[i].node; + nodeB = errors[i].closest.ref.node; + indexA = i; + indexB = errors[i].closest.ref.index; + + context.report({ + node: nodeA, + message: MISPOSITION_MESSAGE, + data: { + propA: getPropertyName(nodeA), + propB: getPropertyName(nodeB), + position: indexA < indexB ? 'before' : 'after' + } }); - default: - return []; + } } - } - - /** - * Compare two properties and find out if they are in the right order - * @param {Array} propertiesInfos Array containing all the properties metadata. - * @param {Object} propA First property name and metadata - * @param {Object} propB Second property name. - * @returns {Object} Object containing a correct true/false flag and the correct indexes for the two properties. - */ - function comparePropsOrder(propertiesInfos, propA, propB) { - var i; - var j; - var k; - var l; - var refIndexA; - var refIndexB; - - // Get references indexes (the correct position) for given properties - var refIndexesA = getRefPropIndexes(propA); - var refIndexesB = getRefPropIndexes(propB); - - // Get current indexes for given properties - var classIndexA = propertiesInfos.indexOf(propA); - var classIndexB = propertiesInfos.indexOf(propB); - - // Loop around the references indexes for the 1st property - for (i = 0, j = refIndexesA.length; i < j; i++) { - refIndexA = refIndexesA[i]; - - // Loop around the properties for the 2nd property (for comparison) - for (k = 0, l = refIndexesB.length; k < l; k++) { - refIndexB = refIndexesB[k]; - - if ( - // Comparing the same properties - refIndexA === refIndexB || - // 1st property is placed before the 2nd one in reference and in current component - refIndexA < refIndexB && classIndexA < classIndexB || - // 1st property is placed after the 2nd one in reference and in current component - refIndexA > refIndexB && classIndexA > classIndexB - ) { - return { - correct: true, - indexA: classIndexA, - indexB: classIndexB - }; - } + /** + * Get properties for a given AST node + * @param {ASTNode} node The AST node being checked. + * @returns {Array} Properties array. + */ + function getComponentProperties(node) { + switch (node.type) { + case 'ClassDeclaration': + return node.body.body; + case 'ObjectExpression': + return node.properties.filter(function(property) { + return property.type === 'Property'; + }); + default: + return []; } } - // We did not find any correct match between reference and current component - return { - correct: false, - indexA: refIndexA, - indexB: refIndexB - }; - } + /** + * Compare two properties and find out if they are in the right order + * @param {Array} propertiesInfos Array containing all the properties metadata. + * @param {Object} propA First property name and metadata + * @param {Object} propB Second property name. + * @returns {Object} Object containing a correct true/false flag and the correct indexes for the two properties. + */ + function comparePropsOrder(propertiesInfos, propA, propB) { + var i; + var j; + var k; + var l; + var refIndexA; + var refIndexB; + + // Get references indexes (the correct position) for given properties + var refIndexesA = getRefPropIndexes(propA); + var refIndexesB = getRefPropIndexes(propB); + + // Get current indexes for given properties + var classIndexA = propertiesInfos.indexOf(propA); + var classIndexB = propertiesInfos.indexOf(propB); + + // Loop around the references indexes for the 1st property + for (i = 0, j = refIndexesA.length; i < j; i++) { + refIndexA = refIndexesA[i]; + + // Loop around the properties for the 2nd property (for comparison) + for (k = 0, l = refIndexesB.length; k < l; k++) { + refIndexB = refIndexesB[k]; + + if ( + // Comparing the same properties + refIndexA === refIndexB || + // 1st property is placed before the 2nd one in reference and in current component + refIndexA < refIndexB && classIndexA < classIndexB || + // 1st property is placed after the 2nd one in reference and in current component + refIndexA > refIndexB && classIndexA > classIndexB + ) { + return { + correct: true, + indexA: classIndexA, + indexB: classIndexB + }; + } + + } + } - /** - * Check properties order from a properties list and store the eventual errors - * @param {Array} properties Array containing all the properties. - */ - function checkPropsOrder(properties) { - var propertiesInfos = properties.map(function(node) { + // We did not find any correct match between reference and current component return { - name: getPropertyName(node), - static: node.static + correct: false, + indexA: refIndexA, + indexB: refIndexB }; - }); - - var i; - var j; - var k; - var l; - var propA; - var propB; - var order; + } - // Loop around the properties - for (i = 0, j = propertiesInfos.length; i < j; i++) { - propA = propertiesInfos[i]; + /** + * Check properties order from a properties list and store the eventual errors + * @param {Array} properties Array containing all the properties. + */ + function checkPropsOrder(properties) { + var propertiesInfos = properties.map(function(node) { + return { + name: getPropertyName(node), + static: node.static + }; + }); - // Loop around the properties a second time (for comparison) - for (k = 0, l = propertiesInfos.length; k < l; k++) { - propB = propertiesInfos[k]; + var i; + var j; + var k; + var l; + var propA; + var propB; + var order; - // Compare the properties order - order = comparePropsOrder(propertiesInfos, propA, propB); + // Loop around the properties + for (i = 0, j = propertiesInfos.length; i < j; i++) { + propA = propertiesInfos[i]; - // Continue to next comparison is order is correct - if (order.correct === true) { - continue; - } + // Loop around the properties a second time (for comparison) + for (k = 0, l = propertiesInfos.length; k < l; k++) { + propB = propertiesInfos[k]; - // Store an error if the order is incorrect - storeError({ - node: properties[i], - index: order.indexA - }, { - node: properties[k], - index: order.indexB - }); - } - } + // Compare the properties order + order = comparePropsOrder(propertiesInfos, propA, propB); - } + // Continue to next comparison is order is correct + if (order.correct === true) { + continue; + } - return { - 'Program:exit': function() { - var list = components.list(); - for (var component in list) { - if (!list.hasOwnProperty(component)) { - continue; + // Store an error if the order is incorrect + storeError({ + node: properties[i], + index: order.indexA + }, { + node: properties[k], + index: order.indexB + }); } - var properties = getComponentProperties(list[component].node); - checkPropsOrder(properties); } - reportErrors(); } - }; -}); - -module.exports.schema = [{ - type: 'object', - properties: { - order: { - type: 'array', - items: { - type: 'string' - } - }, - groups: { - type: 'object', - patternProperties: { - '^.*$': { - type: 'array', - items: { - type: 'string' + return { + 'Program:exit': function() { + var list = components.list(); + for (var component in list) { + if (!list.hasOwnProperty(component)) { + continue; } + var properties = getComponentProperties(list[component].node); + checkPropsOrder(properties); } + + reportErrors(); } - } - }, - additionalProperties: false -}]; + }; + + }) +}; diff --git a/lib/rules/sort-prop-types.js b/lib/rules/sort-prop-types.js index f5ec0ca3db..b16915ba84 100644 --- a/lib/rules/sort-prop-types.js +++ b/lib/rules/sort-prop-types.js @@ -7,162 +7,168 @@ // Rule Definition // ------------------------------------------------------------------------------ -module.exports = function(context) { - - var sourceCode = context.getSourceCode(); - var configuration = context.options[0] || {}; - var requiredFirst = configuration.requiredFirst || false; - var callbacksLast = configuration.callbacksLast || false; - var ignoreCase = configuration.ignoreCase || false; - - /** - * Checks if node is `propTypes` declaration - * @param {ASTNode} node The AST node being checked. - * @returns {Boolean} True if node is `propTypes` declaration, false if not. - */ - function isPropTypesDeclaration(node) { - - // Special case for class properties - // (babel-eslint does not expose property name so we have to rely on tokens) - if (node.type === 'ClassProperty') { - var tokens = context.getFirstTokens(node, 2); - return (tokens[0] && tokens[0].value === 'propTypes') || - (tokens[1] && tokens[1].value === 'propTypes'); - } +module.exports = { + meta: { + docs: {}, + + schema: [{ + type: 'object', + properties: { + requiredFirst: { + type: 'boolean' + }, + callbacksLast: { + type: 'boolean' + }, + ignoreCase: { + type: 'boolean' + } + }, + additionalProperties: false + }] + }, - return Boolean( - node && - node.name === 'propTypes' - ); - } + create: function(context) { + + var sourceCode = context.getSourceCode(); + var configuration = context.options[0] || {}; + var requiredFirst = configuration.requiredFirst || false; + var callbacksLast = configuration.callbacksLast || false; + var ignoreCase = configuration.ignoreCase || false; + + /** + * Checks if node is `propTypes` declaration + * @param {ASTNode} node The AST node being checked. + * @returns {Boolean} True if node is `propTypes` declaration, false if not. + */ + function isPropTypesDeclaration(node) { + + // Special case for class properties + // (babel-eslint does not expose property name so we have to rely on tokens) + if (node.type === 'ClassProperty') { + var tokens = context.getFirstTokens(node, 2); + return (tokens[0] && tokens[0].value === 'propTypes') || + (tokens[1] && tokens[1].value === 'propTypes'); + } - function getKey(node) { - return sourceCode.getText(node.key || node.argument); - } + return Boolean( + node && + node.name === 'propTypes' + ); + } - function getValueName(node) { - return node.type === 'Property' && node.value.property && node.value.property.name; - } + function getKey(node) { + return sourceCode.getText(node.key || node.argument); + } - function isCallbackPropName(propName) { - return /^on[A-Z]/.test(propName); - } + function getValueName(node) { + return node.type === 'Property' && node.value.property && node.value.property.name; + } - function isRequiredProp(node) { - return getValueName(node) === 'isRequired'; - } + function isCallbackPropName(propName) { + return /^on[A-Z]/.test(propName); + } - /** - * Checks if propTypes declarations are sorted - * @param {Array} declarations The array of AST nodes being checked. - * @returns {void} - */ - function checkSorted(declarations) { - declarations.reduce(function(prev, curr, idx, decls) { - if (/SpreadProperty$/.test(curr.type)) { - return decls[idx + 1]; - } + function isRequiredProp(node) { + return getValueName(node) === 'isRequired'; + } - var prevPropName = getKey(prev); - var currentPropName = getKey(curr); - var previousIsRequired = isRequiredProp(prev); - var currentIsRequired = isRequiredProp(curr); - var previousIsCallback = isCallbackPropName(prevPropName); - var currentIsCallback = isCallbackPropName(currentPropName); + /** + * Checks if propTypes declarations are sorted + * @param {Array} declarations The array of AST nodes being checked. + * @returns {void} + */ + function checkSorted(declarations) { + declarations.reduce(function(prev, curr, idx, decls) { + if (/SpreadProperty$/.test(curr.type)) { + return decls[idx + 1]; + } - if (ignoreCase) { - prevPropName = prevPropName.toLowerCase(); - currentPropName = currentPropName.toLowerCase(); - } + var prevPropName = getKey(prev); + var currentPropName = getKey(curr); + var previousIsRequired = isRequiredProp(prev); + var currentIsRequired = isRequiredProp(curr); + var previousIsCallback = isCallbackPropName(prevPropName); + var currentIsCallback = isCallbackPropName(currentPropName); - if (requiredFirst) { - if (previousIsRequired && !currentIsRequired) { - // Transition between required and non-required. Don't compare for alphabetical. - return curr; + if (ignoreCase) { + prevPropName = prevPropName.toLowerCase(); + currentPropName = currentPropName.toLowerCase(); } - if (!previousIsRequired && currentIsRequired) { - // Encountered a non-required prop after a required prop - context.report({ - node: curr, - message: 'Required prop types must be listed before all other prop types' - }); - return curr; + + if (requiredFirst) { + if (previousIsRequired && !currentIsRequired) { + // Transition between required and non-required. Don't compare for alphabetical. + return curr; + } + if (!previousIsRequired && currentIsRequired) { + // Encountered a non-required prop after a required prop + context.report({ + node: curr, + message: 'Required prop types must be listed before all other prop types' + }); + return curr; + } } - } - if (callbacksLast) { - if (!previousIsCallback && currentIsCallback) { - // Entering the callback prop section - return curr; + if (callbacksLast) { + if (!previousIsCallback && currentIsCallback) { + // Entering the callback prop section + return curr; + } + if (previousIsCallback && !currentIsCallback) { + // Encountered a non-callback prop after a callback prop + context.report({ + node: prev, + message: 'Callback prop types must be listed after all other prop types' + }); + return prev; + } } - if (previousIsCallback && !currentIsCallback) { - // Encountered a non-callback prop after a callback prop + + if (currentPropName < prevPropName) { context.report({ - node: prev, - message: 'Callback prop types must be listed after all other prop types' + node: curr, + message: 'Prop types declarations should be sorted alphabetically' }); return prev; } - } - - if (currentPropName < prevPropName) { - context.report({ - node: curr, - message: 'Prop types declarations should be sorted alphabetically' - }); - return prev; - } - - return curr; - }, declarations[0]); - } - - return { - ClassProperty: function(node) { - if (isPropTypesDeclaration(node) && node.value && node.value.type === 'ObjectExpression') { - checkSorted(node.value.properties); - } - }, - - MemberExpression: function(node) { - if (isPropTypesDeclaration(node.property)) { - var right = node.parent.right; - if (right && right.type === 'ObjectExpression') { - checkSorted(right.properties); - } - } - }, - ObjectExpression: function(node) { - node.properties.forEach(function(property) { - if (!property.key) { - return; - } + return curr; + }, declarations[0]); + } - if (!isPropTypesDeclaration(property.key)) { - return; + return { + ClassProperty: function(node) { + if (isPropTypesDeclaration(node) && node.value && node.value.type === 'ObjectExpression') { + checkSorted(node.value.properties); } - if (property.value.type === 'ObjectExpression') { - checkSorted(property.value.properties); + }, + + MemberExpression: function(node) { + if (isPropTypesDeclaration(node.property)) { + var right = node.parent.right; + if (right && right.type === 'ObjectExpression') { + checkSorted(right.properties); + } } - }); - } + }, + + ObjectExpression: function(node) { + node.properties.forEach(function(property) { + if (!property.key) { + return; + } + + if (!isPropTypesDeclaration(property.key)) { + return; + } + if (property.value.type === 'ObjectExpression') { + checkSorted(property.value.properties); + } + }); + } - }; + }; + } }; - -module.exports.schema = [{ - type: 'object', - properties: { - requiredFirst: { - type: 'boolean' - }, - callbacksLast: { - type: 'boolean' - }, - ignoreCase: { - type: 'boolean' - } - }, - additionalProperties: false -}]; diff --git a/lib/rules/wrap-multilines.js b/lib/rules/wrap-multilines.js index f1266e2e87..8dc875f44d 100644 --- a/lib/rules/wrap-multilines.js +++ b/lib/rules/wrap-multilines.js @@ -13,34 +13,40 @@ var util = require('util'); var jsxWrapMultilines = require('./jsx-wrap-multilines'); var isWarnedForDeprecation = false; -module.exports = function(context) { - return util._extend(jsxWrapMultilines(context), { - Program: function() { - if (isWarnedForDeprecation || /\=-(f|-format)=/.test(process.argv.join('='))) { - return; - } - - /* eslint-disable no-console */ - console.log('The react/wrap-multilines rule is deprecated. Please ' + - 'use the react/jsx-wrap-multilines rule instead.'); - /* eslint-enable no-console */ - isWarnedForDeprecation = true; - } - }); -}; +module.exports = { + meta: { + docs: {}, -module.exports.schema = [{ - type: 'object', - properties: { - declaration: { - type: 'boolean' - }, - assignment: { - type: 'boolean' - }, - return: { - type: 'boolean' - } + schema: [{ + type: 'object', + properties: { + declaration: { + type: 'boolean' + }, + assignment: { + type: 'boolean' + }, + return: { + type: 'boolean' + } + }, + additionalProperties: false + }] }, - additionalProperties: false -}]; + + create: function(context) { + return util._extend(jsxWrapMultilines(context), { + Program: function() { + if (isWarnedForDeprecation || /\=-(f|-format)=/.test(process.argv.join('='))) { + return; + } + + /* eslint-disable no-console */ + console.log('The react/wrap-multilines rule is deprecated. Please ' + + 'use the react/jsx-wrap-multilines rule instead.'); + /* eslint-enable no-console */ + isWarnedForDeprecation = true; + } + }); + } +};