[d565449] | 1 | /**
|
---|
| 2 | * @fileoverview Validate props indentation in JSX
|
---|
| 3 | * @author Yannick Croissant
|
---|
| 4 |
|
---|
| 5 | * This rule has been ported and modified from eslint and nodeca.
|
---|
| 6 | * @author Vitaly Puzrin
|
---|
| 7 | * @author Gyandeep Singh
|
---|
| 8 | * @copyright 2015 Vitaly Puzrin. All rights reserved.
|
---|
| 9 | * @copyright 2015 Gyandeep Singh. All rights reserved.
|
---|
| 10 | Copyright (C) 2014 by Vitaly Puzrin
|
---|
| 11 |
|
---|
| 12 | Permission is hereby granted, free of charge, to any person obtaining a copy
|
---|
| 13 | of this software and associated documentation files (the 'Software'), to deal
|
---|
| 14 | in the Software without restriction, including without limitation the rights
|
---|
| 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
---|
| 16 | copies of the Software, and to permit persons to whom the Software is
|
---|
| 17 | furnished to do so, subject to the following conditions:
|
---|
| 18 |
|
---|
| 19 | The above copyright notice and this permission notice shall be included in
|
---|
| 20 | all copies or substantial portions of the Software.
|
---|
| 21 |
|
---|
| 22 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
---|
| 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
---|
| 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
---|
| 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
---|
| 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
---|
| 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
---|
| 28 | THE SOFTWARE.
|
---|
| 29 | */
|
---|
| 30 |
|
---|
| 31 | 'use strict';
|
---|
| 32 |
|
---|
| 33 | const repeat = require('string.prototype.repeat');
|
---|
| 34 |
|
---|
| 35 | const astUtil = require('../util/ast');
|
---|
| 36 | const docsUrl = require('../util/docsUrl');
|
---|
| 37 | const getText = require('../util/eslint').getText;
|
---|
| 38 | const reportC = require('../util/report');
|
---|
| 39 |
|
---|
| 40 | // ------------------------------------------------------------------------------
|
---|
| 41 | // Rule Definition
|
---|
| 42 | // ------------------------------------------------------------------------------
|
---|
| 43 |
|
---|
| 44 | const messages = {
|
---|
| 45 | wrongIndent: 'Expected indentation of {{needed}} {{type}} {{characters}} but found {{gotten}}.',
|
---|
| 46 | };
|
---|
| 47 |
|
---|
| 48 | module.exports = {
|
---|
| 49 | meta: {
|
---|
| 50 | docs: {
|
---|
| 51 | description: 'Enforce props indentation in JSX',
|
---|
| 52 | category: 'Stylistic Issues',
|
---|
| 53 | recommended: false,
|
---|
| 54 | url: docsUrl('jsx-indent-props'),
|
---|
| 55 | },
|
---|
| 56 | fixable: 'code',
|
---|
| 57 |
|
---|
| 58 | messages,
|
---|
| 59 |
|
---|
| 60 | schema: [{
|
---|
| 61 | anyOf: [{
|
---|
| 62 | enum: ['tab', 'first'],
|
---|
| 63 | }, {
|
---|
| 64 | type: 'integer',
|
---|
| 65 | }, {
|
---|
| 66 | type: 'object',
|
---|
| 67 | properties: {
|
---|
| 68 | indentMode: {
|
---|
| 69 | anyOf: [{
|
---|
| 70 | enum: ['tab', 'first'],
|
---|
| 71 | }, {
|
---|
| 72 | type: 'integer',
|
---|
| 73 | }],
|
---|
| 74 | },
|
---|
| 75 | ignoreTernaryOperator: {
|
---|
| 76 | type: 'boolean',
|
---|
| 77 | },
|
---|
| 78 | },
|
---|
| 79 | }],
|
---|
| 80 | }],
|
---|
| 81 | },
|
---|
| 82 |
|
---|
| 83 | create(context) {
|
---|
| 84 | const extraColumnStart = 0;
|
---|
| 85 | let indentType = 'space';
|
---|
| 86 | /** @type {number|'first'} */
|
---|
| 87 | let indentSize = 4;
|
---|
| 88 | const line = {
|
---|
| 89 | isUsingOperator: false,
|
---|
| 90 | currentOperator: false,
|
---|
| 91 | };
|
---|
| 92 | let ignoreTernaryOperator = false;
|
---|
| 93 |
|
---|
| 94 | if (context.options.length) {
|
---|
| 95 | const isConfigObject = typeof context.options[0] === 'object';
|
---|
| 96 | const indentMode = isConfigObject
|
---|
| 97 | ? context.options[0].indentMode
|
---|
| 98 | : context.options[0];
|
---|
| 99 |
|
---|
| 100 | if (indentMode === 'first') {
|
---|
| 101 | indentSize = 'first';
|
---|
| 102 | indentType = 'space';
|
---|
| 103 | } else if (indentMode === 'tab') {
|
---|
| 104 | indentSize = 1;
|
---|
| 105 | indentType = 'tab';
|
---|
| 106 | } else if (typeof indentMode === 'number') {
|
---|
| 107 | indentSize = indentMode;
|
---|
| 108 | indentType = 'space';
|
---|
| 109 | }
|
---|
| 110 |
|
---|
| 111 | if (isConfigObject && context.options[0].ignoreTernaryOperator) {
|
---|
| 112 | ignoreTernaryOperator = true;
|
---|
| 113 | }
|
---|
| 114 | }
|
---|
| 115 |
|
---|
| 116 | /**
|
---|
| 117 | * Reports a given indent violation and properly pluralizes the message
|
---|
| 118 | * @param {ASTNode} node Node violating the indent rule
|
---|
| 119 | * @param {Number} needed Expected indentation character count
|
---|
| 120 | * @param {Number} gotten Indentation character count in the actual node/code
|
---|
| 121 | */
|
---|
| 122 | function report(node, needed, gotten) {
|
---|
| 123 | const msgContext = {
|
---|
| 124 | needed,
|
---|
| 125 | type: indentType,
|
---|
| 126 | characters: needed === 1 ? 'character' : 'characters',
|
---|
| 127 | gotten,
|
---|
| 128 | };
|
---|
| 129 |
|
---|
| 130 | reportC(context, messages.wrongIndent, 'wrongIndent', {
|
---|
| 131 | node,
|
---|
| 132 | data: msgContext,
|
---|
| 133 | fix(fixer) {
|
---|
| 134 | return fixer.replaceTextRange([node.range[0] - node.loc.start.column, node.range[0]],
|
---|
| 135 | repeat(indentType === 'space' ? ' ' : '\t', needed)
|
---|
| 136 | );
|
---|
| 137 | },
|
---|
| 138 | });
|
---|
| 139 | }
|
---|
| 140 |
|
---|
| 141 | /**
|
---|
| 142 | * Get node indent
|
---|
| 143 | * @param {ASTNode} node Node to examine
|
---|
| 144 | * @return {Number} Indent
|
---|
| 145 | */
|
---|
| 146 | function getNodeIndent(node) {
|
---|
| 147 | let src = getText(context, node, node.loc.start.column + extraColumnStart);
|
---|
| 148 | const lines = src.split('\n');
|
---|
| 149 | src = lines[0];
|
---|
| 150 |
|
---|
| 151 | let regExp;
|
---|
| 152 | if (indentType === 'space') {
|
---|
| 153 | regExp = /^[ ]+/;
|
---|
| 154 | } else {
|
---|
| 155 | regExp = /^[\t]+/;
|
---|
| 156 | }
|
---|
| 157 |
|
---|
| 158 | const indent = regExp.exec(src);
|
---|
| 159 | const useOperator = /^([ ]|[\t])*[:]/.test(src) || /^([ ]|[\t])*[?]/.test(src);
|
---|
| 160 | const useBracket = /[<]/.test(src);
|
---|
| 161 |
|
---|
| 162 | line.currentOperator = false;
|
---|
| 163 | if (useOperator) {
|
---|
| 164 | line.isUsingOperator = true;
|
---|
| 165 | line.currentOperator = true;
|
---|
| 166 | } else if (useBracket) {
|
---|
| 167 | line.isUsingOperator = false;
|
---|
| 168 | }
|
---|
| 169 |
|
---|
| 170 | return indent ? indent[0].length : 0;
|
---|
| 171 | }
|
---|
| 172 |
|
---|
| 173 | /**
|
---|
| 174 | * Check indent for nodes list
|
---|
| 175 | * @param {ASTNode[]} nodes list of node objects
|
---|
| 176 | * @param {Number} indent needed indent
|
---|
| 177 | */
|
---|
| 178 | function checkNodesIndent(nodes, indent) {
|
---|
| 179 | let nestedIndent = indent;
|
---|
| 180 | nodes.forEach((node) => {
|
---|
| 181 | const nodeIndent = getNodeIndent(node);
|
---|
| 182 | if (
|
---|
| 183 | line.isUsingOperator
|
---|
| 184 | && !line.currentOperator
|
---|
| 185 | && indentSize !== 'first'
|
---|
| 186 | && !ignoreTernaryOperator
|
---|
| 187 | ) {
|
---|
| 188 | nestedIndent += indentSize;
|
---|
| 189 | line.isUsingOperator = false;
|
---|
| 190 | }
|
---|
| 191 | if (
|
---|
| 192 | node.type !== 'ArrayExpression' && node.type !== 'ObjectExpression'
|
---|
| 193 | && nodeIndent !== nestedIndent && astUtil.isNodeFirstInLine(context, node)
|
---|
| 194 | ) {
|
---|
| 195 | report(node, nestedIndent, nodeIndent);
|
---|
| 196 | }
|
---|
| 197 | });
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 | return {
|
---|
| 201 | JSXOpeningElement(node) {
|
---|
| 202 | if (!node.attributes.length) {
|
---|
| 203 | return;
|
---|
| 204 | }
|
---|
| 205 | let propIndent;
|
---|
| 206 | if (indentSize === 'first') {
|
---|
| 207 | const firstPropNode = node.attributes[0];
|
---|
| 208 | propIndent = firstPropNode.loc.start.column;
|
---|
| 209 | } else {
|
---|
| 210 | const elementIndent = getNodeIndent(node);
|
---|
| 211 | propIndent = elementIndent + indentSize;
|
---|
| 212 | }
|
---|
| 213 | checkNodesIndent(node.attributes, propIndent);
|
---|
| 214 | },
|
---|
| 215 | };
|
---|
| 216 | },
|
---|
| 217 | };
|
---|