/** * @fileoverview A rule to ensure blank lines within blocks. * @author Mathias Schreck * @copyright 2014 Mathias Schreck. All rights reserved. */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = function(context) { var requirePadding = context.options[0] !== "never"; var ALWAYS_MESSAGE = "Block must be padded by blank lines.", NEVER_MESSAGE = "Block must not be padded by blank lines."; /** * Checks if the location of a node or token is before the location of another node or token * @param {ASTNode|Token} a The node or token to check if its location is before b. * @param {ASTNode|Token} b The node or token which will be compared with a. * @returns {boolean} True if a is located before b. */ function isLocatedBefore(a, b) { return a.range[1] < b.range[0]; } /** * Checks if the given non empty block node has a blank line before its first child node. * @param {ASTNode} node The AST node of a BlockStatement. * @returns {boolean} Whether or not the block starts with a blank line. */ function isBlockTopPadded(node) { var blockStart = node.loc.start.line, first = node.body[0], firstLine = first.loc.start.line, expectedFirstLine = blockStart + 2, leadingComments = (node.body[0].leadingComments || []).slice(), firstComment; while (leadingComments.length > 0 && leadingComments[0].loc.start.line <= node.loc.start.line) { leadingComments.shift(); } firstComment = leadingComments[0]; if (firstComment && isLocatedBefore(firstComment, first)) { firstLine = firstComment.loc.start.line; } return expectedFirstLine <= firstLine; } /** * Checks if the given non empty block node has a blank line after its last child node. * @param {ASTNode} node The AST node of a BlockStatement. * @returns {boolean} Whether or not the block ends with a blank line. */ function isBlockBottomPadded(node) { var blockEnd = node.loc.end.line, last = node.body[node.body.length - 1], lastToken = context.getLastToken(last), lastLine = lastToken.loc.end.line, expectedLastLine = blockEnd - 2, trailingComments = (node.body[node.body.length - 1].trailingComments || []).slice(), lastComment; while (trailingComments.length > 0 && trailingComments[trailingComments.length - 1].loc.end.line >= node.loc.end.line) { trailingComments.pop(); } lastComment = trailingComments[trailingComments.length - 1]; if (lastComment && isLocatedBefore(lastToken, lastComment)) { lastLine = lastComment.loc.end.line; } return lastLine <= expectedLastLine; } /** * Checks the given BlockStatement node to be padded if the block is not empty. * @param {ASTNode} node The AST node of a BlockStatement. * @returns {void} undefined. */ function checkPadding(node) { if (node.body.length > 0) { var blockHasTopPadding = isBlockTopPadded(node), blockHasBottomPadding = isBlockBottomPadded(node); if (requirePadding) { if (!blockHasTopPadding) { context.report(node, ALWAYS_MESSAGE); } if (!blockHasBottomPadding) { context.report({ node: node, loc: {line: node.loc.end.line, column: node.loc.end.column - 1 }, message: ALWAYS_MESSAGE }); } } else { if (blockHasTopPadding) { context.report(node, NEVER_MESSAGE); } if (blockHasBottomPadding) { context.report({ node: node, loc: {line: node.loc.end.line, column: node.loc.end.column - 1 }, message: NEVER_MESSAGE }); } } } } return { "BlockStatement": checkPadding }; }; module.exports.schema = [ { "enum": ["always", "never"] } ];