diff --git a/README.md b/README.md index 437406b571..d8b38fd571 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,18 @@ Add `plugins` section and specify ESLint-plugin-React as a plugin. } ``` +You can also specify some settings that will be shared across all the plugin rules. + +```js +{ + "settings": { + "react": { + "pragma": "React" // Pragma to use, default to "React" + } + } +} +``` + If it is not already the case you must also configure `ESLint` to support JSX. With ESLint 1.x.x: diff --git a/docs/rules/jsx-uses-react.md b/docs/rules/jsx-uses-react.md index 7ff269a4a6..a454bd1004 100644 --- a/docs/rules/jsx-uses-react.md +++ b/docs/rules/jsx-uses-react.md @@ -40,7 +40,6 @@ var Foo = require('foo'); var Hello =
Hello {this.props.name}
; ``` - ## Rule Options ```js @@ -51,6 +50,8 @@ var Hello =
Hello {this.props.name}
; ### `pragma` +**Deprecation notice**: This option is deprecated, please use the [shared settings](README.md#configuration) to specify a custom pragma. + As an alternative to specifying the above pragma in each source file, you can specify this configuration option: diff --git a/lib/rules/jsx-uses-react.js b/lib/rules/jsx-uses-react.js index 2e4839801d..c5fd74204c 100644 --- a/lib/rules/jsx-uses-react.js +++ b/lib/rules/jsx-uses-react.js @@ -5,17 +5,15 @@ 'use strict'; var variableUtil = require('../util/variable'); +var pragmaUtil = require('../util/pragma'); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ -var JSX_ANNOTATION_REGEX = /^\*\s*@jsx\s+([^\s]+)/; - module.exports = function(context) { - var config = context.options[0] || {}; - var id = config.pragma || 'React'; + var pragma = pragmaUtil.getFromContext(context); // -------------------------------------------------------------------------- // Public @@ -24,15 +22,11 @@ module.exports = function(context) { return { JSXOpeningElement: function() { - variableUtil.markVariableAsUsed(context, id); + variableUtil.markVariableAsUsed(context, pragma); }, BlockComment: function(node) { - var matches = JSX_ANNOTATION_REGEX.exec(node.value); - if (!matches) { - return; - } - id = matches[1].split('.')[0]; + pragma = pragmaUtil.getFromNode(node) || pragma; } }; diff --git a/lib/rules/react-in-jsx-scope.js b/lib/rules/react-in-jsx-scope.js index de92fce142..550fddc3b0 100644 --- a/lib/rules/react-in-jsx-scope.js +++ b/lib/rules/react-in-jsx-scope.js @@ -5,36 +5,31 @@ 'use strict'; var variableUtil = require('../util/variable'); +var pragmaUtil = require('../util/pragma'); // ----------------------------------------------------------------------------- // Rule Definition // ----------------------------------------------------------------------------- -var JSX_ANNOTATION_REGEX = /^\*\s*@jsx\s+([^\s]+)/; - module.exports = function(context) { - var id = 'React'; + var pragma = pragmaUtil.getFromContext(context); var NOT_DEFINED_MESSAGE = '\'{{name}}\' must be in scope when using JSX'; return { JSXOpeningElement: function(node) { var variables = variableUtil.variablesInScope(context); - if (variableUtil.findVariable(variables, id)) { + if (variableUtil.findVariable(variables, pragma)) { return; } context.report(node, NOT_DEFINED_MESSAGE, { - name: id + name: pragma }); }, BlockComment: function(node) { - var matches = JSX_ANNOTATION_REGEX.exec(node.value); - if (!matches) { - return; - } - id = matches[1].split('.')[0]; + pragma = pragmaUtil.getFromNode(node) || pragma; } }; diff --git a/lib/util/Components.js b/lib/util/Components.js index c6823b569f..4222a36192 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -6,6 +6,7 @@ var util = require('util'); var variableUtil = require('./variable'); +var pragmaUtil = require('./pragma'); /** * Components @@ -104,6 +105,7 @@ Components.prototype.length = function() { function componentRule(rule, context) { + var pragma = pragmaUtil.getFromContext(context); var sourceCode = context.getSourceCode(); var components = new Components(); @@ -120,7 +122,7 @@ function componentRule(rule, context) { if (!node.parent) { return false; } - return /^(React\.)?createClass$/.test(sourceCode.getText(node.parent.callee)); + return new RegExp('^(' + pragma + '\\.)?createClass$').test(sourceCode.getText(node.parent.callee)); }, /** @@ -133,7 +135,7 @@ function componentRule(rule, context) { if (!node.superClass) { return false; } - return /^(React\.)?Component$/.test(sourceCode.getText(node.superClass)); + return new RegExp('^(' + pragma + '\\.)?Component$').test(sourceCode.getText(node.superClass)); }, /** @@ -374,6 +376,10 @@ function componentRule(rule, context) { components.add(node, 0); }, + BlockComment: function(node) { + pragma = pragmaUtil.getFromNode(node) || pragma; + }, + ReturnStatement: function(node) { if (!utils.isReturningJSX(node)) { return; diff --git a/lib/util/pragma.js b/lib/util/pragma.js new file mode 100644 index 0000000000..479257f47f --- /dev/null +++ b/lib/util/pragma.js @@ -0,0 +1,32 @@ +/** + * @fileoverview Utility functions for React pragma configuration + * @author Yannick Croissant + */ +'use strict'; + +var JSX_ANNOTATION_REGEX = /^\*\s*@jsx\s+([^\s]+)/; + +function getFromContext(context) { + var pragma = 'React'; + // .eslintrc shared settings (http://eslint.org/docs/user-guide/configuring#adding-shared-settings) + if (context.settings.react && context.settings.react.pragma) { + pragma = context.settings.react.pragma; + // Deprecated pragma option, here for backward compatibility + } else if (context.options[0] && context.options[0].pragma) { + pragma = context.options[0].pragma; + } + return pragma.split('.')[0]; +} + +function getFromNode(node) { + var matches = JSX_ANNOTATION_REGEX.exec(node.value); + if (!matches) { + return false; + } + return matches[1].split('.')[0]; +} + +module.exports = { + getFromContext: getFromContext, + getFromNode: getFromNode +}; diff --git a/tests/lib/rules/display-name.js b/tests/lib/rules/display-name.js index 0490397f02..5033a0fa96 100644 --- a/tests/lib/rules/display-name.js +++ b/tests/lib/rules/display-name.js @@ -21,6 +21,12 @@ var parserOptions = { } }; +var settings = { + react: { + pragma: 'Foo' + } +}; + // ------------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------------ @@ -417,6 +423,38 @@ ruleTester.run('display-name', rule, { errors: [{ message: 'Component definition is missing display name' }] + }, { + code: [ + 'var Hello = Foo.createClass({', + ' _renderHello: function() {', + ' return Hello {this.props.name};', + ' },', + ' render: function() {', + ' return
{this._renderHello()}
;', + ' }', + '});' + ].join('\n'), + parser: 'babel-eslint', + settings: settings, + errors: [{ + message: 'Component definition is missing display name' + }] + }, { + code: [ + '/** @jsx Foo */', + 'var Hello = Foo.createClass({', + ' _renderHello: function() {', + ' return Hello {this.props.name};', + ' },', + ' render: function() {', + ' return
{this._renderHello()}
;', + ' }', + '});' + ].join('\n'), + parser: 'babel-eslint', + errors: [{ + message: 'Component definition is missing display name' + }] }, { code: [ 'const Mixin = {', diff --git a/tests/lib/rules/jsx-uses-react.js b/tests/lib/rules/jsx-uses-react.js index a8d7db2633..709cfdd879 100644 --- a/tests/lib/rules/jsx-uses-react.js +++ b/tests/lib/rules/jsx-uses-react.js @@ -20,6 +20,12 @@ var parserOptions = { } }; +var settings = { + react: { + pragma: 'Foo' + } +}; + // ----------------------------------------------------------------------------- // Tests // ----------------------------------------------------------------------------- @@ -31,12 +37,15 @@ ruleTester.run('no-unused-vars', rule, { {code: '/*eslint jsx-uses-react:1*/ var React;
;', parserOptions: parserOptions}, {code: '/*eslint jsx-uses-react:1*/ var React; (function () {
})();', parserOptions: parserOptions}, {code: '/*eslint jsx-uses-react:1*/ /** @jsx Foo */ var Foo;
;', parserOptions: parserOptions}, - {code: '/*eslint jsx-uses-react:[1,{"pragma":"Foo"}]*/ var Foo;
;', parserOptions: parserOptions} + {code: '/*eslint jsx-uses-react:[1,{"pragma":"Foo"}]*/ var Foo;
;', parserOptions: parserOptions}, + {code: '/*eslint jsx-uses-react:1*/ var Foo;
;', settings: settings, parserOptions: parserOptions} ], invalid: [ {code: '/*eslint jsx-uses-react:1*/ var React;', errors: [{message: '"React" is defined but never used'}], parserOptions: parserOptions}, {code: '/*eslint jsx-uses-react:1*/ /** @jsx Foo */ var React;
;', - errors: [{message: '"React" is defined but never used'}], parserOptions: parserOptions} + errors: [{message: '"React" is defined but never used'}], parserOptions: parserOptions}, + {code: '/*eslint jsx-uses-react:1*/ var React;
;', + errors: [{message: '"React" is defined but never used'}], settings: settings, parserOptions: parserOptions} ] }); diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index bb20dfe4ac..63d5bc2364 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -19,6 +19,12 @@ var parserOptions = { } }; +var settings = { + react: { + pragma: 'Foo' + } +}; + require('babel-eslint'); // ------------------------------------------------------------------------------ @@ -1426,6 +1432,42 @@ ruleTester.run('prop-types', rule, { errors: [ {message: '\'lastname\' is missing in props validation'} ] + }, { + code: [ + 'class Test extends Foo.Component {', + ' render() {', + ' return (', + '
{this.props.firstname} {this.props.lastname}
', + ' );', + ' }', + '}', + 'Test.propTypes = {', + ' firstname: React.PropTypes.string', + '};' + ].join('\n'), + parser: 'babel-eslint', + settings: settings, + errors: [ + {message: '\'lastname\' is missing in props validation'} + ] + }, { + code: [ + '/** @jsx Foo */', + 'class Test extends Foo.Component {', + ' render() {', + ' return (', + '
{this.props.firstname} {this.props.lastname}
', + ' );', + ' }', + '}', + 'Test.propTypes = {', + ' firstname: React.PropTypes.string', + '};' + ].join('\n'), + parser: 'babel-eslint', + errors: [ + {message: '\'lastname\' is missing in props validation'} + ] } ] }); diff --git a/tests/lib/rules/react-in-jsx-scope.js b/tests/lib/rules/react-in-jsx-scope.js index 33f8df2ce5..4dbb28c2bd 100644 --- a/tests/lib/rules/react-in-jsx-scope.js +++ b/tests/lib/rules/react-in-jsx-scope.js @@ -20,6 +20,12 @@ var parserOptions = { } }; +var settings = { + react: { + pragma: 'Foo' + } +}; + // ----------------------------------------------------------------------------- // Tests // ----------------------------------------------------------------------------- @@ -46,7 +52,8 @@ ruleTester.run('react-in-jsx-scope', rule, { '});', 'export default Button;' ].join('\n'), - parserOptions: parserOptions} + parserOptions: parserOptions}, + {code: 'var Foo, App; ;', settings: settings, parserOptions: parserOptions} ], invalid: [ {code: 'var App, a = ;', @@ -58,6 +65,8 @@ ruleTester.run('react-in-jsx-scope', rule, { {code: '/** @jsx React.DOM */ var a = ;', errors: [{message: '\'React\' must be in scope when using JSX'}], parserOptions: parserOptions}, {code: '/** @jsx Foo.bar */ var React, a = ;', - errors: [{message: '\'Foo\' must be in scope when using JSX'}], parserOptions: parserOptions} + errors: [{message: '\'Foo\' must be in scope when using JSX'}], parserOptions: parserOptions}, + {code: 'var React, a = ;', + errors: [{message: '\'Foo\' must be in scope when using JSX'}], settings: settings, parserOptions: parserOptions} ] });