diff --git a/.gitignore b/.gitignore index 123ae94..d532efc 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules + +.idea diff --git a/README.md b/README.md index d674389..c64fc48 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Finally, enable all of the rules that you would like to use. "react-native/split-platform-components": 2, "react-native/no-inline-styles": 2, "react-native/no-color-literals": 2, + "react-native/no-raw-text": 2, } } ``` @@ -81,6 +82,7 @@ Finally, enable all of the rules that you would like to use. * [split-platform-components](docs/rules/split-platform-components.md): Enforce using platform specific filenames when necessary * [no-inline-styles](docs/rules/no-inline-styles.md): Detect JSX components with inline styles that contain literal values * [no-color-literals](docs/rules/no-color-literals.md): Detect `StyleSheet` rules and inline styles containing color literals instead of variables +* [no-raw-text](docs/rules/no-raw-text.md): Detect raw text outside of `Text` component [npm-url]: https://npmjs.org/package/eslint-plugin-react-native [npm-image]: http://img.shields.io/npm/v/eslint-plugin-react-native.svg?style=flat-square diff --git a/docs/rules/no-raw-text.md b/docs/rules/no-raw-text.md new file mode 100644 index 0000000..c5a038e --- /dev/null +++ b/docs/rules/no-raw-text.md @@ -0,0 +1,26 @@ +# Detect raw text outside of Text component +All strings in React Native should be wrapped with a Text component. + +## Rule Details + +The following patterns are considered warnings: + +```js +some text +``` + +```js +const text = 'some text'; +{`${text}`} +``` + +The following patterns are not considered warnings: + +```js +some text +``` + +```js +const text = 'some text'; +{`${text}`} +``` diff --git a/index.js b/index.js index ddbc6e1..abc99bc 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ const allRules = { 'no-inline-styles': require('./lib/rules/no-inline-styles'), 'no-color-literals': require('./lib/rules/no-color-literals'), 'split-platform-components': require('./lib/rules/split-platform-components'), + 'no-raw-text': require('./lib/rules/no-raw-text'), }; function configureAsError(rules) { @@ -30,6 +31,7 @@ module.exports = { 'no-inline-styles': 0, 'no-color-literals': 0, 'split-platform-components': 0, + 'no-raw-text': 0, }, environments: { 'react-native': { diff --git a/lib/rules/no-raw-text.js b/lib/rules/no-raw-text.js new file mode 100644 index 0000000..4c1340c --- /dev/null +++ b/lib/rules/no-raw-text.js @@ -0,0 +1,63 @@ +/** + * @fileoverview Detects raw text outside of Text component + * @author Alex Zhukov + */ + +'use strict'; + +module.exports = (context) => { + const elementName = node => ( + node.openingElement && + node.openingElement.name && + node.openingElement.name.type === 'JSXIdentifier' && + node.openingElement.name.name + ); + + const report = (node) => { + const errorValue = node.type === 'TemplateLiteral' + ? `TemplateLiteral: ${node.expressions[0].name}` + : node.value; + context.report({ + node, + message: `Raw text (${errorValue.trim()}) cannot be used outside of a tag`, + }); + }; + + const getValidation = node => elementName(node.parent) !== 'Text'; + + return { + Literal(node) { + const parentType = node.parent.type; + const onlyFor = ['JSXExpressionContainer', 'JSXElement']; + if (/^[\s]+$/.test(node.value) || + typeof node.value !== 'string' || + !onlyFor.includes(parentType) || + (node.parent.parent && node.parent.parent.type === 'JSXAttribute') + ) return; + + const isStringLiteral = parentType === 'JSXExpressionContainer'; + if (getValidation(isStringLiteral ? node.parent : node)) { + report(node); + } + }, + + JSXText(node) { + if (getValidation(node)) { + report(node); + } + }, + + TemplateLiteral(node) { + if ( + node.parent.type !== 'JSXExpressionContainer' || + (node.parent.parent && node.parent.parent.type === 'JSXAttribute') + ) return; + + if (getValidation(node.parent)) { + report(node); + } + }, + }; +}; + +module.exports.schema = []; diff --git a/tests/lib/rules/no-raw-text.js b/tests/lib/rules/no-raw-text.js new file mode 100644 index 0000000..dc25f8b --- /dev/null +++ b/tests/lib/rules/no-raw-text.js @@ -0,0 +1,108 @@ +/** + * @fileoverview Detects raw text outside of Text component + * @author Alex Zhukov + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-raw-text'); +const RuleTester = require('eslint').RuleTester; + +require('babel-eslint'); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); + +const tests = { + valid: [ + { + code: ` + export default class MyComponent extends Component { + render() { + return (some text); + } + } + `, + }, + { + code: ` + export default class MyComponent extends Component { + render() { + const text = 'some text'; + return ({\`\${text}\`}); + } + } + `, + }, + { + code: ` + export default class MyComponent extends Component { + render() { + return ({'some text'}); + } + } + `, + }, + ], + invalid: [ + { + code: ` + export default class MyComponent extends Component { + render() { + return (some text); + } + } + `, + errors: [{ + message: 'Raw text (some text) cannot be used outside of a tag', + }], + }, + { + code: ` + export default class MyComponent extends Component { + render() { + const text = 'some text'; + return ({\`\${text}\`}); + } + } + `, + errors: [{ + message: 'Raw text (TemplateLiteral: text) cannot be used outside of a tag', + }], + }, + { + code: ` + export default class MyComponent extends Component { + render() { + return ({'some text'}); + } + } + `, + errors: [{ + message: 'Raw text (some text) cannot be used outside of a tag', + }], + }, + ], +}; + +const config = { + parser: 'babel-eslint', + parserOptions: { + ecmaFeatures: { + classes: true, + jsx: true, + }, + }, +}; + +tests.valid.forEach(t => Object.assign(t, config)); +tests.invalid.forEach(t => Object.assign(t, config)); + +ruleTester.run('no-raw-text', rule, tests);