UNPKG

9.94 kBJavaScriptView Raw
1/**
2 * @fileoverview Comma style - enforces comma styles of two types: last and first
3 * @author Vignesh Anand aka vegetableman
4 */
5
6"use strict";
7
8const astUtils = require("../ast-utils");
9
10//------------------------------------------------------------------------------
11// Rule Definition
12//------------------------------------------------------------------------------
13
14module.exports = {
15 meta: {
16 docs: {
17 description: "enforce consistent comma style",
18 category: "Stylistic Issues",
19 recommended: false
20 },
21 fixable: "code",
22 schema: [
23 {
24 enum: ["first", "last"]
25 },
26 {
27 type: "object",
28 properties: {
29 exceptions: {
30 type: "object",
31 additionalProperties: {
32 type: "boolean"
33 }
34 }
35 },
36 additionalProperties: false
37 }
38 ]
39 },
40
41 create(context) {
42 const style = context.options[0] || "last",
43 sourceCode = context.getSourceCode();
44 let exceptions = {};
45
46 if (context.options.length === 2 && context.options[1].hasOwnProperty("exceptions")) {
47 exceptions = context.options[1].exceptions;
48 }
49
50 //--------------------------------------------------------------------------
51 // Helpers
52 //--------------------------------------------------------------------------
53
54 /**
55 * Determines if a given token is a comma operator.
56 * @param {ASTNode} token The token to check.
57 * @returns {boolean} True if the token is a comma, false if not.
58 * @private
59 */
60 function isComma(token) {
61 return !!token && (token.type === "Punctuator") && (token.value === ",");
62 }
63
64 /**
65 * Modified text based on the style
66 * @param {string} styleType Style type
67 * @param {string} text Source code text
68 * @returns {string} modified text
69 * @private
70 */
71 function getReplacedText(styleType, text) {
72 switch (styleType) {
73 case "between":
74 return `,${text.replace("\n", "")}`;
75
76 case "first":
77 return `${text},`;
78
79 case "last":
80 return `,${text}`;
81
82 default:
83 return "";
84 }
85 }
86
87 /**
88 * Determines the fixer function for a given style.
89 * @param {string} styleType comma style
90 * @param {ASTNode} previousItemToken The token to check.
91 * @param {ASTNode} commaToken The token to check.
92 * @param {ASTNode} currentItemToken The token to check.
93 * @returns {Function} Fixer function
94 * @private
95 */
96 function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) {
97 const text =
98 sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) +
99 sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]);
100 const range = [previousItemToken.range[1], currentItemToken.range[0]];
101
102 return function(fixer) {
103 return fixer.replaceTextRange(range, getReplacedText(styleType, text));
104 };
105 }
106
107 /**
108 * Validates the spacing around single items in lists.
109 * @param {Token} previousItemToken The last token from the previous item.
110 * @param {Token} commaToken The token representing the comma.
111 * @param {Token} currentItemToken The first token of the current item.
112 * @param {Token} reportItem The item to use when reporting an error.
113 * @returns {void}
114 * @private
115 */
116 function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) {
117
118 // if single line
119 if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
120 astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
121
122 return;
123
124 } else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
125 !astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
126
127 // lone comma
128 context.report({
129 node: reportItem,
130 loc: {
131 line: commaToken.loc.end.line,
132 column: commaToken.loc.start.column
133 },
134 message: "Bad line breaking before and after ','.",
135 fix: getFixerFunction("between", previousItemToken, commaToken, currentItemToken)
136 });
137
138 } else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
139
140 context.report({
141 node: reportItem,
142 message: "',' should be placed first.",
143 fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
144 });
145
146 } else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
147
148 context.report({
149 node: reportItem,
150 loc: {
151 line: commaToken.loc.end.line,
152 column: commaToken.loc.end.column
153 },
154 message: "',' should be placed last.",
155 fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
156 });
157 }
158 }
159
160 /**
161 * Checks the comma placement with regards to a declaration/property/element
162 * @param {ASTNode} node The binary expression node to check
163 * @param {string} property The property of the node containing child nodes.
164 * @private
165 * @returns {void}
166 */
167 function validateComma(node, property) {
168 const items = node[property],
169 arrayLiteral = (node.type === "ArrayExpression");
170
171 if (items.length > 1 || arrayLiteral) {
172
173 // seed as opening [
174 let previousItemToken = sourceCode.getFirstToken(node);
175
176 items.forEach(function(item) {
177 const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken,
178 currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken),
179 reportItem = item || currentItemToken,
180 tokenBeforeComma = sourceCode.getTokenBefore(commaToken);
181
182 // Check if previous token is wrapped in parentheses
183 if (tokenBeforeComma && tokenBeforeComma.value === ")") {
184 previousItemToken = tokenBeforeComma;
185 }
186
187 /*
188 * This works by comparing three token locations:
189 * - previousItemToken is the last token of the previous item
190 * - commaToken is the location of the comma before the current item
191 * - currentItemToken is the first token of the current item
192 *
193 * These values get switched around if item is undefined.
194 * previousItemToken will refer to the last token not belonging
195 * to the current item, which could be a comma or an opening
196 * square bracket. currentItemToken could be a comma.
197 *
198 * All comparisons are done based on these tokens directly, so
199 * they are always valid regardless of an undefined item.
200 */
201 if (isComma(commaToken)) {
202 validateCommaItemSpacing(previousItemToken, commaToken,
203 currentItemToken, reportItem);
204 }
205
206 previousItemToken = item ? sourceCode.getLastToken(item) : previousItemToken;
207 });
208
209 /*
210 * Special case for array literals that have empty last items, such
211 * as [ 1, 2, ]. These arrays only have two items show up in the
212 * AST, so we need to look at the token to verify that there's no
213 * dangling comma.
214 */
215 if (arrayLiteral) {
216
217 const lastToken = sourceCode.getLastToken(node),
218 nextToLastToken = sourceCode.getTokenBefore(lastToken);
219
220 if (isComma(nextToLastToken)) {
221 validateCommaItemSpacing(
222 sourceCode.getTokenBefore(nextToLastToken),
223 nextToLastToken,
224 lastToken,
225 lastToken
226 );
227 }
228 }
229 }
230 }
231
232 //--------------------------------------------------------------------------
233 // Public
234 //--------------------------------------------------------------------------
235
236 const nodes = {};
237
238 if (!exceptions.VariableDeclaration) {
239 nodes.VariableDeclaration = function(node) {
240 validateComma(node, "declarations");
241 };
242 }
243 if (!exceptions.ObjectExpression) {
244 nodes.ObjectExpression = function(node) {
245 validateComma(node, "properties");
246 };
247 }
248 if (!exceptions.ArrayExpression) {
249 nodes.ArrayExpression = function(node) {
250 validateComma(node, "elements");
251 };
252 }
253
254 return nodes;
255 }
256};