/** * @fileoverview Comma style - enforces comma styles of two types: last and first * @author Vignesh Anand aka vegetableman * @copyright 2014 Vignesh Anand. All rights reserved. * @copyright 2015 Evan Simmons. All rights reserved. */ "use strict"; var astUtils = require("../ast-utils"); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = function(context) { var style = context.options[0] || "last", exceptions = {}; if (context.options.length === 2 && context.options[1].hasOwnProperty("exceptions")) { exceptions = context.options[1].exceptions; } //-------------------------------------------------------------------------- // Helpers //-------------------------------------------------------------------------- /** * Determines if a given token is a comma operator. * @param {ASTNode} token The token to check. * @returns {boolean} True if the token is a comma, false if not. * @private */ function isComma(token) { return !!token && (token.type === "Punctuator") && (token.value === ","); } /** * Validates the spacing around single items in lists. * @param {Token} previousItemToken The last token from the previous item. * @param {Token} commaToken The token representing the comma. * @param {Token} currentItemToken The first token of the current item. * @param {Token} reportItem The item to use when reporting an error. * @returns {void} * @private */ function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) { // if single line if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) && astUtils.isTokenOnSameLine(previousItemToken, commaToken)) { return; } else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) && !astUtils.isTokenOnSameLine(previousItemToken, commaToken)) { // lone comma context.report(reportItem, { line: commaToken.loc.end.line, column: commaToken.loc.start.column }, "Bad line breaking before and after ','."); } else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) { context.report(reportItem, "',' should be placed first."); } else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) { context.report(reportItem, { line: commaToken.loc.end.line, column: commaToken.loc.end.column }, "',' should be placed last."); } } /** * Checks the comma placement with regards to a declaration/property/element * @param {ASTNode} node The binary expression node to check * @param {string} property The property of the node containing child nodes. * @private * @returns {void} */ function validateComma(node, property) { var items = node[property], arrayLiteral = (node.type === "ArrayExpression"), previousItemToken; if (items.length > 1 || arrayLiteral) { // seed as opening [ previousItemToken = context.getFirstToken(node); items.forEach(function(item) { var commaToken = item ? context.getTokenBefore(item) : previousItemToken, currentItemToken = item ? context.getFirstToken(item) : context.getTokenAfter(commaToken), reportItem = item || currentItemToken; /* * This works by comparing three token locations: * - previousItemToken is the last token of the previous item * - commaToken is the location of the comma before the current item * - currentItemToken is the first token of the current item * * These values get switched around if item is undefined. * previousItemToken will refer to the last token not belonging * to the current item, which could be a comma or an opening * square bracket. currentItemToken could be a comma. * * All comparisons are done based on these tokens directly, so * they are always valid regardless of an undefined item. */ if (isComma(commaToken)) { validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem); } previousItemToken = item ? context.getLastToken(item) : previousItemToken; }); /* * Special case for array literals that have empty last items, such * as [ 1, 2, ]. These arrays only have two items show up in the * AST, so we need to look at the token to verify that there's no * dangling comma. */ if (arrayLiteral) { var lastToken = context.getLastToken(node), nextToLastToken = context.getTokenBefore(lastToken); if (isComma(nextToLastToken)) { validateCommaItemSpacing( context.getTokenBefore(nextToLastToken), nextToLastToken, lastToken, lastToken ); } } } } //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- var nodes = {}; if (!exceptions.VariableDeclaration) { nodes.VariableDeclaration = function(node) { validateComma(node, "declarations"); }; } if (!exceptions.ObjectExpression) { nodes.ObjectExpression = function(node) { validateComma(node, "properties"); }; } if (!exceptions.ArrayExpression) { nodes.ArrayExpression = function(node) { validateComma(node, "elements"); }; } return nodes; }; module.exports.schema = [ { "enum": ["first", "last"] }, { "type": "object", "properties": { "exceptions": { "type": "object", "additionalProperties": { "type": "boolean" } } }, "additionalProperties": false } ];