1/** 2 * @fileoverview A rule to choose between single and double quote marks 3 * @author Matt DuVall <http://www.mattduvall.com/>, Brandon Payton 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Constants 16//------------------------------------------------------------------------------ 17 18const QUOTE_SETTINGS = { 19 double: { 20 quote: "\"", 21 alternateQuote: "'", 22 description: "doublequote" 23 }, 24 single: { 25 quote: "'", 26 alternateQuote: "\"", 27 description: "singlequote" 28 }, 29 backtick: { 30 quote: "`", 31 alternateQuote: "\"", 32 description: "backtick" 33 } 34}; 35 36// An unescaped newline is a newline preceded by an even number of backslashes. 37const UNESCAPED_LINEBREAK_PATTERN = new RegExp(String.raw`(^|[^\\])(\\\\)*[${Array.from(astUtils.LINEBREAKS).join("")}]`, "u"); 38 39/** 40 * Switches quoting of javascript string between ' " and ` 41 * escaping and unescaping as necessary. 42 * Only escaping of the minimal set of characters is changed. 43 * Note: escaping of newlines when switching from backtick to other quotes is not handled. 44 * @param {string} str A string to convert. 45 * @returns {string} The string with changed quotes. 46 * @private 47 */ 48QUOTE_SETTINGS.double.convert = 49QUOTE_SETTINGS.single.convert = 50QUOTE_SETTINGS.backtick.convert = function(str) { 51 const newQuote = this.quote; 52 const oldQuote = str[0]; 53 54 if (newQuote === oldQuote) { 55 return str; 56 } 57 return newQuote + str.slice(1, -1).replace(/\\(\$\{|\r\n?|\n|.)|["'`]|\$\{|(\r\n?|\n)/gu, (match, escaped, newline) => { 58 if (escaped === oldQuote || oldQuote === "`" && escaped === "${") { 59 return escaped; // unescape 60 } 61 if (match === newQuote || newQuote === "`" && match === "${") { 62 return `\\${match}`; // escape 63 } 64 if (newline && oldQuote === "`") { 65 return "\\n"; // escape newlines 66 } 67 return match; 68 }) + newQuote; 69}; 70 71const AVOID_ESCAPE = "avoid-escape"; 72 73//------------------------------------------------------------------------------ 74// Rule Definition 75//------------------------------------------------------------------------------ 76 77module.exports = { 78 meta: { 79 type: "layout", 80 81 docs: { 82 description: "enforce the consistent use of either backticks, double, or single quotes", 83 category: "Stylistic Issues", 84 recommended: false, 85 url: "https://eslint.org/docs/rules/quotes" 86 }, 87 88 fixable: "code", 89 90 schema: [ 91 { 92 enum: ["single", "double", "backtick"] 93 }, 94 { 95 anyOf: [ 96 { 97 enum: ["avoid-escape"] 98 }, 99 { 100 type: "object", 101 properties: { 102 avoidEscape: { 103 type: "boolean" 104 }, 105 allowTemplateLiterals: { 106 type: "boolean" 107 } 108 }, 109 additionalProperties: false 110 } 111 ] 112 } 113 ], 114 115 messages: { 116 wrongQuotes: "Strings must use {{description}}." 117 } 118 }, 119 120 create(context) { 121 122 const quoteOption = context.options[0], 123 settings = QUOTE_SETTINGS[quoteOption || "double"], 124 options = context.options[1], 125 allowTemplateLiterals = options && options.allowTemplateLiterals === true, 126 sourceCode = context.getSourceCode(); 127 let avoidEscape = options && options.avoidEscape === true; 128 129 // deprecated 130 if (options === AVOID_ESCAPE) { 131 avoidEscape = true; 132 } 133 134 /** 135 * Determines if a given node is part of JSX syntax. 136 * 137 * This function returns `true` in the following cases: 138 * 139 * - `<div className="foo"></div>` ... If the literal is an attribute value, the parent of the literal is `JSXAttribute`. 140 * - `<div>foo</div>` ... If the literal is a text content, the parent of the literal is `JSXElement`. 141 * - `<>foo</>` ... If the literal is a text content, the parent of the literal is `JSXFragment`. 142 * 143 * In particular, this function returns `false` in the following cases: 144 * 145 * - `<div className={"foo"}></div>` 146 * - `<div>{"foo"}</div>` 147 * 148 * In both cases, inside of the braces is handled as normal JavaScript. 149 * The braces are `JSXExpressionContainer` nodes. 150 * @param {ASTNode} node The Literal node to check. 151 * @returns {boolean} True if the node is a part of JSX, false if not. 152 * @private 153 */ 154 function isJSXLiteral(node) { 155 return node.parent.type === "JSXAttribute" || node.parent.type === "JSXElement" || node.parent.type === "JSXFragment"; 156 } 157 158 /** 159 * Checks whether or not a given node is a directive. 160 * The directive is a `ExpressionStatement` which has only a string literal. 161 * @param {ASTNode} node A node to check. 162 * @returns {boolean} Whether or not the node is a directive. 163 * @private 164 */ 165 function isDirective(node) { 166 return ( 167 node.type === "ExpressionStatement" && 168 node.expression.type === "Literal" && 169 typeof node.expression.value === "string" 170 ); 171 } 172 173 /** 174 * Checks whether or not a given node is a part of directive prologues. 175 * See also: http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive 176 * @param {ASTNode} node A node to check. 177 * @returns {boolean} Whether or not the node is a part of directive prologues. 178 * @private 179 */ 180 function isPartOfDirectivePrologue(node) { 181 const block = node.parent.parent; 182 183 if (block.type !== "Program" && (block.type !== "BlockStatement" || !astUtils.isFunction(block.parent))) { 184 return false; 185 } 186 187 // Check the node is at a prologue. 188 for (let i = 0; i < block.body.length; ++i) { 189 const statement = block.body[i]; 190 191 if (statement === node.parent) { 192 return true; 193 } 194 if (!isDirective(statement)) { 195 break; 196 } 197 } 198 199 return false; 200 } 201 202 /** 203 * Checks whether or not a given node is allowed as non backtick. 204 * @param {ASTNode} node A node to check. 205 * @returns {boolean} Whether or not the node is allowed as non backtick. 206 * @private 207 */ 208 function isAllowedAsNonBacktick(node) { 209 const parent = node.parent; 210 211 switch (parent.type) { 212 213 // Directive Prologues. 214 case "ExpressionStatement": 215 return isPartOfDirectivePrologue(node); 216 217 // LiteralPropertyName. 218 case "Property": 219 case "MethodDefinition": 220 return parent.key === node && !parent.computed; 221 222 // ModuleSpecifier. 223 case "ImportDeclaration": 224 case "ExportNamedDeclaration": 225 case "ExportAllDeclaration": 226 return parent.source === node; 227 228 // Others don't allow. 229 default: 230 return false; 231 } 232 } 233 234 /** 235 * Checks whether or not a given TemplateLiteral node is actually using any of the special features provided by template literal strings. 236 * @param {ASTNode} node A TemplateLiteral node to check. 237 * @returns {boolean} Whether or not the TemplateLiteral node is using any of the special features provided by template literal strings. 238 * @private 239 */ 240 function isUsingFeatureOfTemplateLiteral(node) { 241 const hasTag = node.parent.type === "TaggedTemplateExpression" && node === node.parent.quasi; 242 243 if (hasTag) { 244 return true; 245 } 246 247 const hasStringInterpolation = node.expressions.length > 0; 248 249 if (hasStringInterpolation) { 250 return true; 251 } 252 253 const isMultilineString = node.quasis.length >= 1 && UNESCAPED_LINEBREAK_PATTERN.test(node.quasis[0].value.raw); 254 255 if (isMultilineString) { 256 return true; 257 } 258 259 return false; 260 } 261 262 return { 263 264 Literal(node) { 265 const val = node.value, 266 rawVal = node.raw; 267 268 if (settings && typeof val === "string") { 269 let isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) || 270 isJSXLiteral(node) || 271 astUtils.isSurroundedBy(rawVal, settings.quote); 272 273 if (!isValid && avoidEscape) { 274 isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0; 275 } 276 277 if (!isValid) { 278 context.report({ 279 node, 280 messageId: "wrongQuotes", 281 data: { 282 description: settings.description 283 }, 284 fix(fixer) { 285 if (quoteOption === "backtick" && astUtils.hasOctalEscapeSequence(rawVal)) { 286 287 // An octal escape sequence in a template literal would produce syntax error, even in non-strict mode. 288 return null; 289 } 290 291 return fixer.replaceText(node, settings.convert(node.raw)); 292 } 293 }); 294 } 295 } 296 }, 297 298 TemplateLiteral(node) { 299 300 // Don't throw an error if backticks are expected or a template literal feature is in use. 301 if ( 302 allowTemplateLiterals || 303 quoteOption === "backtick" || 304 isUsingFeatureOfTemplateLiteral(node) 305 ) { 306 return; 307 } 308 309 context.report({ 310 node, 311 messageId: "wrongQuotes", 312 data: { 313 description: settings.description 314 }, 315 fix(fixer) { 316 if (isPartOfDirectivePrologue(node)) { 317 318 /* 319 * TemplateLiterals in a directive prologue aren't actually directives, but if they're 320 * in the directive prologue, then fixing them might turn them into directives and change 321 * the behavior of the code. 322 */ 323 return null; 324 } 325 return fixer.replaceText(node, settings.convert(sourceCode.getText(node))); 326 } 327 }); 328 } 329 }; 330 331 } 332}; 333