1 | /**
|
2 | * @fileoverview A rule to choose between single and double quote marks
|
3 | * @author Matt DuVall <http://www.mattduvall.com/>, Brandon Payton
|
4 | * @copyright 2013 Matt DuVall. All rights reserved.
|
5 | * See LICENSE file in root directory for full license.
|
6 | */
|
7 |
|
8 | ;
|
9 |
|
10 | //------------------------------------------------------------------------------
|
11 | // Requirements
|
12 | //------------------------------------------------------------------------------
|
13 |
|
14 | var astUtils = require("../ast-utils");
|
15 |
|
16 | //------------------------------------------------------------------------------
|
17 | // Constants
|
18 | //------------------------------------------------------------------------------
|
19 |
|
20 | var QUOTE_SETTINGS = {
|
21 | "double": {
|
22 | quote: "\"",
|
23 | alternateQuote: "'",
|
24 | description: "doublequote"
|
25 | },
|
26 | "single": {
|
27 | quote: "'",
|
28 | alternateQuote: "\"",
|
29 | description: "singlequote"
|
30 | },
|
31 | "backtick": {
|
32 | quote: "`",
|
33 | alternateQuote: "\"",
|
34 | description: "backtick"
|
35 | }
|
36 | };
|
37 | /**
|
38 | * Switches quoting of javascript string between ' " and `
|
39 | * escaping and unescaping as necessary.
|
40 | * Only escaping of the minimal set of characters is changed.
|
41 | * Note: escaping of newlines when switching from backtick to other quotes is not handled.
|
42 | * @param {string} str - A string to convert.
|
43 | * @returns {string} The string with changed quotes.
|
44 | * @private
|
45 | */
|
46 | QUOTE_SETTINGS.double.convert =
|
47 | QUOTE_SETTINGS.single.convert =
|
48 | QUOTE_SETTINGS.backtick.convert = function(str) {
|
49 | var newQuote = this.quote;
|
50 | var oldQuote = str[0];
|
51 | if (newQuote === oldQuote) {
|
52 | return str;
|
53 | }
|
54 | return newQuote + str.slice(1, -1).replace(/\\(\${|\r\n?|\n|.)|["'`]|\${|(\r\n?|\n)/g, function(match, escaped, newline) {
|
55 | if (escaped === oldQuote || oldQuote === "`" && escaped === "${") {
|
56 | return escaped; // unescape
|
57 | }
|
58 | if (match === newQuote || newQuote === "`" && match === "${") {
|
59 | return "\\" + match; // escape
|
60 | }
|
61 | if (newline && oldQuote === "`") {
|
62 | return "\\n"; // escape newlines
|
63 | }
|
64 | return match;
|
65 | }) + newQuote;
|
66 | };
|
67 |
|
68 | var AVOID_ESCAPE = "avoid-escape",
|
69 | FUNCTION_TYPE = /^(?:Arrow)?Function(?:Declaration|Expression)$/;
|
70 |
|
71 | //------------------------------------------------------------------------------
|
72 | // Rule Definition
|
73 | //------------------------------------------------------------------------------
|
74 |
|
75 | module.exports = function(context) {
|
76 |
|
77 | var quoteOption = context.options[0],
|
78 | settings = QUOTE_SETTINGS[quoteOption || "double"],
|
79 | avoidEscape = context.options[1] === AVOID_ESCAPE,
|
80 | sourceCode = context.getSourceCode();
|
81 |
|
82 | /**
|
83 | * Determines if a given node is part of JSX syntax.
|
84 | * @param {ASTNode} node The node to check.
|
85 | * @returns {boolean} True if the node is a JSX node, false if not.
|
86 | * @private
|
87 | */
|
88 | function isJSXElement(node) {
|
89 | return node.type.indexOf("JSX") === 0;
|
90 | }
|
91 |
|
92 | /**
|
93 | * Checks whether or not a given node is a directive.
|
94 | * The directive is a `ExpressionStatement` which has only a string literal.
|
95 | * @param {ASTNode} node - A node to check.
|
96 | * @returns {boolean} Whether or not the node is a directive.
|
97 | * @private
|
98 | */
|
99 | function isDirective(node) {
|
100 | return (
|
101 | node.type === "ExpressionStatement" &&
|
102 | node.expression.type === "Literal" &&
|
103 | typeof node.expression.value === "string"
|
104 | );
|
105 | }
|
106 |
|
107 | /**
|
108 | * Checks whether or not a given node is a part of directive prologues.
|
109 | * See also: http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive
|
110 | * @param {ASTNode} node - A node to check.
|
111 | * @returns {boolean} Whether or not the node is a part of directive prologues.
|
112 | * @private
|
113 | */
|
114 | function isPartOfDirectivePrologue(node) {
|
115 | var block = node.parent.parent;
|
116 | if (block.type !== "Program" && (block.type !== "BlockStatement" || !FUNCTION_TYPE.test(block.parent.type))) {
|
117 | return false;
|
118 | }
|
119 |
|
120 | // Check the node is at a prologue.
|
121 | for (var i = 0; i < block.body.length; ++i) {
|
122 | var statement = block.body[i];
|
123 |
|
124 | if (statement === node.parent) {
|
125 | return true;
|
126 | }
|
127 | if (!isDirective(statement)) {
|
128 | break;
|
129 | }
|
130 | }
|
131 |
|
132 | return false;
|
133 | }
|
134 |
|
135 | /**
|
136 | * Checks whether or not a given node is allowed as non backtick.
|
137 | * @param {ASTNode} node - A node to check.
|
138 | * @returns {boolean} Whether or not the node is allowed as non backtick.
|
139 | * @private
|
140 | */
|
141 | function isAllowedAsNonBacktick(node) {
|
142 | var parent = node.parent;
|
143 |
|
144 | switch (parent.type) {
|
145 | // Directive Prologues.
|
146 | case "ExpressionStatement":
|
147 | return isPartOfDirectivePrologue(node);
|
148 |
|
149 | // LiteralPropertyName.
|
150 | case "Property":
|
151 | return parent.key === node && !parent.computed;
|
152 |
|
153 | // ModuleSpecifier.
|
154 | case "ImportDeclaration":
|
155 | case "ExportNamedDeclaration":
|
156 | case "ExportAllDeclaration":
|
157 | return parent.source === node;
|
158 |
|
159 | // Others don't allow.
|
160 | default:
|
161 | return false;
|
162 | }
|
163 | }
|
164 |
|
165 | return {
|
166 |
|
167 | "Literal": function(node) {
|
168 | var val = node.value,
|
169 | rawVal = node.raw,
|
170 | isValid;
|
171 |
|
172 | if (settings && typeof val === "string") {
|
173 | isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) ||
|
174 | isJSXElement(node.parent) ||
|
175 | astUtils.isSurroundedBy(rawVal, settings.quote);
|
176 |
|
177 | if (!isValid && avoidEscape) {
|
178 | isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0;
|
179 | }
|
180 |
|
181 | if (!isValid) {
|
182 | context.report({
|
183 | node: node,
|
184 | message: "Strings must use " + settings.description + ".",
|
185 | fix: function(fixer) {
|
186 | return fixer.replaceText(node, settings.convert(node.raw));
|
187 | }
|
188 | });
|
189 | }
|
190 | }
|
191 | },
|
192 |
|
193 | "TemplateLiteral": function(node) {
|
194 |
|
195 | // If backticks are expected or it's a tagged template, then this shouldn't throw an errors
|
196 | if (quoteOption === "backtick" || node.parent.type === "TaggedTemplateExpression") {
|
197 | return;
|
198 | }
|
199 |
|
200 | var shouldWarn = node.quasis.length === 1 && (node.quasis[0].value.cooked.indexOf("\n") === -1);
|
201 |
|
202 | if (shouldWarn) {
|
203 | context.report({
|
204 | node: node,
|
205 | message: "Strings must use " + settings.description + ".",
|
206 | fix: function(fixer) {
|
207 | return fixer.replaceText(node, settings.convert(sourceCode.getText(node)));
|
208 | }
|
209 | });
|
210 | }
|
211 | }
|
212 | };
|
213 |
|
214 | };
|
215 |
|
216 | module.exports.schema = [
|
217 | {
|
218 | "enum": ["single", "double", "backtick"]
|
219 | },
|
220 | {
|
221 | "enum": ["avoid-escape"]
|
222 | }
|
223 | ];
|