[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Prevent using string literals in React component definition
|
---|
| 3 | * @author Caleb Morris
|
---|
| 4 | * @author David Buchan-Swanson
|
---|
| 5 | */
|
---|
| 6 |
|
---|
| 7 | 'use strict';
|
---|
| 8 |
|
---|
| 9 | const iterFrom = require('es-iterator-helpers/Iterator.from');
|
---|
| 10 | const map = require('es-iterator-helpers/Iterator.prototype.map');
|
---|
| 11 |
|
---|
| 12 | const docsUrl = require('../util/docsUrl');
|
---|
| 13 | const report = require('../util/report');
|
---|
| 14 | const getText = require('../util/eslint').getText;
|
---|
| 15 |
|
---|
| 16 | // ------------------------------------------------------------------------------
|
---|
| 17 | // Rule Definition
|
---|
| 18 | // ------------------------------------------------------------------------------
|
---|
| 19 |
|
---|
| 20 | function trimIfString(val) {
|
---|
| 21 | return typeof val === 'string' ? val.trim() : val;
|
---|
| 22 | }
|
---|
| 23 |
|
---|
| 24 | const messages = {
|
---|
| 25 | invalidPropValue: 'Invalid prop value: "{{text}}"',
|
---|
| 26 | noStringsInAttributes: 'Strings not allowed in attributes: "{{text}}"',
|
---|
| 27 | noStringsInJSX: 'Strings not allowed in JSX files: "{{text}}"',
|
---|
| 28 | literalNotInJSXExpression: 'Missing JSX expression container around literal string: "{{text}}"',
|
---|
| 29 | };
|
---|
| 30 |
|
---|
| 31 | /** @type {import('eslint').Rule.RuleModule} */
|
---|
| 32 | module.exports = {
|
---|
| 33 | meta: {
|
---|
| 34 | docs: {
|
---|
| 35 | description: 'Disallow usage of string literals in JSX',
|
---|
| 36 | category: 'Stylistic Issues',
|
---|
| 37 | recommended: false,
|
---|
| 38 | url: docsUrl('jsx-no-literals'),
|
---|
| 39 | },
|
---|
| 40 |
|
---|
| 41 | messages,
|
---|
| 42 |
|
---|
| 43 | schema: [{
|
---|
| 44 | type: 'object',
|
---|
| 45 | properties: {
|
---|
| 46 | noStrings: {
|
---|
| 47 | type: 'boolean',
|
---|
| 48 | },
|
---|
| 49 | allowedStrings: {
|
---|
| 50 | type: 'array',
|
---|
| 51 | uniqueItems: true,
|
---|
| 52 | items: {
|
---|
| 53 | type: 'string',
|
---|
| 54 | },
|
---|
| 55 | },
|
---|
| 56 | ignoreProps: {
|
---|
| 57 | type: 'boolean',
|
---|
| 58 | },
|
---|
| 59 | noAttributeStrings: {
|
---|
| 60 | type: 'boolean',
|
---|
| 61 | },
|
---|
| 62 | },
|
---|
| 63 | additionalProperties: false,
|
---|
| 64 | }],
|
---|
| 65 | },
|
---|
| 66 |
|
---|
| 67 | create(context) {
|
---|
| 68 | const defaults = {
|
---|
| 69 | noStrings: false,
|
---|
| 70 | allowedStrings: [],
|
---|
| 71 | ignoreProps: false,
|
---|
| 72 | noAttributeStrings: false,
|
---|
| 73 | };
|
---|
| 74 | const config = Object.assign({}, defaults, context.options[0] || {});
|
---|
| 75 | config.allowedStrings = new Set(map(iterFrom(config.allowedStrings), trimIfString));
|
---|
| 76 |
|
---|
| 77 | function defaultMessageId() {
|
---|
| 78 | const ancestorIsJSXElement = arguments.length >= 1 && arguments[0];
|
---|
| 79 | if (config.noAttributeStrings && !ancestorIsJSXElement) {
|
---|
| 80 | return 'noStringsInAttributes';
|
---|
| 81 | }
|
---|
| 82 | if (config.noStrings) {
|
---|
| 83 | return 'noStringsInJSX';
|
---|
| 84 | }
|
---|
| 85 | return 'literalNotInJSXExpression';
|
---|
| 86 | }
|
---|
| 87 |
|
---|
| 88 | function getParentIgnoringBinaryExpressions(node) {
|
---|
| 89 | let current = node;
|
---|
| 90 | while (current.parent.type === 'BinaryExpression') {
|
---|
| 91 | current = current.parent;
|
---|
| 92 | }
|
---|
| 93 | return current.parent;
|
---|
| 94 | }
|
---|
| 95 |
|
---|
| 96 | function getValidation(node) {
|
---|
| 97 | const values = [trimIfString(node.raw), trimIfString(node.value)];
|
---|
| 98 | if (values.some((value) => config.allowedStrings.has(value))) {
|
---|
| 99 | return false;
|
---|
| 100 | }
|
---|
| 101 |
|
---|
| 102 | const parent = getParentIgnoringBinaryExpressions(node);
|
---|
| 103 |
|
---|
| 104 | function isParentNodeStandard() {
|
---|
| 105 | if (!/^[\s]+$/.test(node.value) && typeof node.value === 'string' && parent.type.includes('JSX')) {
|
---|
| 106 | if (config.noAttributeStrings) {
|
---|
| 107 | return parent.type === 'JSXAttribute' || parent.type === 'JSXElement';
|
---|
| 108 | }
|
---|
| 109 | if (!config.noAttributeStrings) {
|
---|
| 110 | return parent.type !== 'JSXAttribute';
|
---|
| 111 | }
|
---|
| 112 | }
|
---|
| 113 |
|
---|
| 114 | return false;
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | const standard = isParentNodeStandard();
|
---|
| 118 |
|
---|
| 119 | if (config.noStrings) {
|
---|
| 120 | return standard;
|
---|
| 121 | }
|
---|
| 122 | return standard && parent.type !== 'JSXExpressionContainer';
|
---|
| 123 | }
|
---|
| 124 |
|
---|
| 125 | function getParentAndGrandParentType(node) {
|
---|
| 126 | const parent = getParentIgnoringBinaryExpressions(node);
|
---|
| 127 | const parentType = parent.type;
|
---|
| 128 | const grandParentType = parent.parent.type;
|
---|
| 129 |
|
---|
| 130 | return {
|
---|
| 131 | parent,
|
---|
| 132 | parentType,
|
---|
| 133 | grandParentType,
|
---|
| 134 | grandParent: parent.parent,
|
---|
| 135 | };
|
---|
| 136 | }
|
---|
| 137 |
|
---|
| 138 | function hasJSXElementParentOrGrandParent(node) {
|
---|
| 139 | const parents = getParentAndGrandParentType(node);
|
---|
| 140 | const parentType = parents.parentType;
|
---|
| 141 | const grandParentType = parents.grandParentType;
|
---|
| 142 |
|
---|
| 143 | return parentType === 'JSXFragment' || parentType === 'JSXElement' || grandParentType === 'JSXElement';
|
---|
| 144 | }
|
---|
| 145 |
|
---|
| 146 | function reportLiteralNode(node, messageId) {
|
---|
| 147 | const ancestorIsJSXElement = hasJSXElementParentOrGrandParent(node);
|
---|
| 148 | messageId = messageId || defaultMessageId(ancestorIsJSXElement);
|
---|
| 149 |
|
---|
| 150 | report(context, messages[messageId], messageId, {
|
---|
| 151 | node,
|
---|
| 152 | data: {
|
---|
| 153 | text: getText(context, node).trim(),
|
---|
| 154 | },
|
---|
| 155 | });
|
---|
| 156 | }
|
---|
| 157 |
|
---|
| 158 | // --------------------------------------------------------------------------
|
---|
| 159 | // Public
|
---|
| 160 | // --------------------------------------------------------------------------
|
---|
| 161 |
|
---|
| 162 | return {
|
---|
| 163 | Literal(node) {
|
---|
| 164 | if (getValidation(node) && (hasJSXElementParentOrGrandParent(node) || !config.ignoreProps)) {
|
---|
| 165 | reportLiteralNode(node);
|
---|
| 166 | }
|
---|
| 167 | },
|
---|
| 168 |
|
---|
| 169 | JSXAttribute(node) {
|
---|
| 170 | const isNodeValueString = node && node.value && node.value.type === 'Literal' && typeof node.value.value === 'string' && !config.allowedStrings.has(node.value.value);
|
---|
| 171 |
|
---|
| 172 | if (config.noStrings && !config.ignoreProps && isNodeValueString) {
|
---|
| 173 | const messageId = 'invalidPropValue';
|
---|
| 174 | reportLiteralNode(node, messageId);
|
---|
| 175 | }
|
---|
| 176 | },
|
---|
| 177 |
|
---|
| 178 | JSXText(node) {
|
---|
| 179 | if (getValidation(node)) {
|
---|
| 180 | reportLiteralNode(node);
|
---|
| 181 | }
|
---|
| 182 | },
|
---|
| 183 |
|
---|
| 184 | TemplateLiteral(node) {
|
---|
| 185 | const parents = getParentAndGrandParentType(node);
|
---|
| 186 | const parentType = parents.parentType;
|
---|
| 187 | const grandParentType = parents.grandParentType;
|
---|
| 188 | const isParentJSXExpressionCont = parentType === 'JSXExpressionContainer';
|
---|
| 189 | const isParentJSXElement = parentType === 'JSXElement' || grandParentType === 'JSXElement';
|
---|
| 190 |
|
---|
| 191 | if (isParentJSXExpressionCont && config.noStrings && (isParentJSXElement || !config.ignoreProps)) {
|
---|
| 192 | reportLiteralNode(node);
|
---|
| 193 | }
|
---|
| 194 | },
|
---|
| 195 | };
|
---|
| 196 | },
|
---|
| 197 | };
|
---|