mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
367 lines
13 KiB
367 lines
13 KiB
/**
|
|
* @fileoverview Enforces empty lines around comments.
|
|
* @author Jamund Ferguson
|
|
*/
|
|
"use strict";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Requirements
|
|
//------------------------------------------------------------------------------
|
|
|
|
const lodash = require("lodash"),
|
|
astUtils = require("../ast-utils");
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Return an array with with any line numbers that are empty.
|
|
* @param {Array} lines An array of each line of the file.
|
|
* @returns {Array} An array of line numbers.
|
|
*/
|
|
function getEmptyLineNums(lines) {
|
|
const emptyLines = lines.map(function(line, i) {
|
|
return {
|
|
code: line.trim(),
|
|
num: i + 1
|
|
};
|
|
}).filter(function(line) {
|
|
return !line.code;
|
|
}).map(function(line) {
|
|
return line.num;
|
|
});
|
|
|
|
return emptyLines;
|
|
}
|
|
|
|
/**
|
|
* Return an array with with any line numbers that contain comments.
|
|
* @param {Array} comments An array of comment nodes.
|
|
* @returns {Array} An array of line numbers.
|
|
*/
|
|
function getCommentLineNums(comments) {
|
|
const lines = [];
|
|
|
|
comments.forEach(function(token) {
|
|
const start = token.loc.start.line;
|
|
const end = token.loc.end.line;
|
|
|
|
lines.push(start, end);
|
|
});
|
|
return lines;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//------------------------------------------------------------------------------
|
|
|
|
module.exports = {
|
|
meta: {
|
|
docs: {
|
|
description: "require empty lines around comments",
|
|
category: "Stylistic Issues",
|
|
recommended: false
|
|
},
|
|
|
|
fixable: "whitespace",
|
|
|
|
schema: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
beforeBlockComment: {
|
|
type: "boolean"
|
|
},
|
|
afterBlockComment: {
|
|
type: "boolean"
|
|
},
|
|
beforeLineComment: {
|
|
type: "boolean"
|
|
},
|
|
afterLineComment: {
|
|
type: "boolean"
|
|
},
|
|
allowBlockStart: {
|
|
type: "boolean"
|
|
},
|
|
allowBlockEnd: {
|
|
type: "boolean"
|
|
},
|
|
allowObjectStart: {
|
|
type: "boolean"
|
|
},
|
|
allowObjectEnd: {
|
|
type: "boolean"
|
|
},
|
|
allowArrayStart: {
|
|
type: "boolean"
|
|
},
|
|
allowArrayEnd: {
|
|
type: "boolean"
|
|
}
|
|
},
|
|
additionalProperties: false
|
|
}
|
|
]
|
|
},
|
|
|
|
create(context) {
|
|
|
|
const options = context.options[0] ? Object.assign({}, context.options[0]) : {};
|
|
|
|
options.beforeLineComment = options.beforeLineComment || false;
|
|
options.afterLineComment = options.afterLineComment || false;
|
|
options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
|
|
options.afterBlockComment = options.afterBlockComment || false;
|
|
options.allowBlockStart = options.allowBlockStart || false;
|
|
options.allowBlockEnd = options.allowBlockEnd || false;
|
|
|
|
const sourceCode = context.getSourceCode();
|
|
|
|
const lines = sourceCode.lines,
|
|
numLines = lines.length + 1,
|
|
comments = sourceCode.getAllComments(),
|
|
commentLines = getCommentLineNums(comments),
|
|
emptyLines = getEmptyLineNums(lines),
|
|
commentAndEmptyLines = commentLines.concat(emptyLines);
|
|
|
|
/**
|
|
* Returns whether or not a token is a comment node type
|
|
* @param {Token} token The token to check
|
|
* @returns {boolean} True if the token is a comment node
|
|
*/
|
|
function isCommentNodeType(token) {
|
|
return token && (token.type === "Block" || token.type === "Line");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are on lines starting with or ending with code
|
|
* @param {ASTNode} node The comment node to check.
|
|
* @returns {boolean} True if the comment is not alone.
|
|
*/
|
|
function codeAroundComment(node) {
|
|
let token;
|
|
|
|
token = node;
|
|
do {
|
|
token = sourceCode.getTokenOrCommentBefore(token);
|
|
} while (isCommentNodeType(token));
|
|
|
|
if (token && astUtils.isTokenOnSameLine(token, node)) {
|
|
return true;
|
|
}
|
|
|
|
token = node;
|
|
do {
|
|
token = sourceCode.getTokenOrCommentAfter(token);
|
|
} while (isCommentNodeType(token));
|
|
|
|
if (token && astUtils.isTokenOnSameLine(node, token)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are inside a node type or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @param {ASTNode} parent The Comment parent node.
|
|
* @param {string} nodeType The parent type to check against.
|
|
* @returns {boolean} True if the comment is inside nodeType.
|
|
*/
|
|
function isCommentInsideNodeType(node, parent, nodeType) {
|
|
return parent.type === nodeType ||
|
|
(parent.body && parent.body.type === nodeType) ||
|
|
(parent.consequent && parent.consequent.type === nodeType);
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the parent start or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @param {string} nodeType The parent type to check against.
|
|
* @returns {boolean} True if the comment is at parent start.
|
|
*/
|
|
function isCommentAtParentStart(node, nodeType) {
|
|
const ancestors = context.getAncestors();
|
|
let parent;
|
|
|
|
if (ancestors.length) {
|
|
parent = ancestors.pop();
|
|
}
|
|
|
|
return parent && isCommentInsideNodeType(node, parent, nodeType) &&
|
|
node.loc.start.line - parent.loc.start.line === 1;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the parent end or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @param {string} nodeType The parent type to check against.
|
|
* @returns {boolean} True if the comment is at parent end.
|
|
*/
|
|
function isCommentAtParentEnd(node, nodeType) {
|
|
const ancestors = context.getAncestors();
|
|
let parent;
|
|
|
|
if (ancestors.length) {
|
|
parent = ancestors.pop();
|
|
}
|
|
|
|
return parent && isCommentInsideNodeType(node, parent, nodeType) &&
|
|
parent.loc.end.line - node.loc.end.line === 1;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the block start or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @returns {boolean} True if the comment is at block start.
|
|
*/
|
|
function isCommentAtBlockStart(node) {
|
|
return isCommentAtParentStart(node, "ClassBody") || isCommentAtParentStart(node, "BlockStatement") || isCommentAtParentStart(node, "SwitchCase");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the block end or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @returns {boolean} True if the comment is at block end.
|
|
*/
|
|
function isCommentAtBlockEnd(node) {
|
|
return isCommentAtParentEnd(node, "ClassBody") || isCommentAtParentEnd(node, "BlockStatement") || isCommentAtParentEnd(node, "SwitchCase") || isCommentAtParentEnd(node, "SwitchStatement");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the object start or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @returns {boolean} True if the comment is at object start.
|
|
*/
|
|
function isCommentAtObjectStart(node) {
|
|
return isCommentAtParentStart(node, "ObjectExpression") || isCommentAtParentStart(node, "ObjectPattern");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the object end or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @returns {boolean} True if the comment is at object end.
|
|
*/
|
|
function isCommentAtObjectEnd(node) {
|
|
return isCommentAtParentEnd(node, "ObjectExpression") || isCommentAtParentEnd(node, "ObjectPattern");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the array start or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @returns {boolean} True if the comment is at array start.
|
|
*/
|
|
function isCommentAtArrayStart(node) {
|
|
return isCommentAtParentStart(node, "ArrayExpression") || isCommentAtParentStart(node, "ArrayPattern");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the array end or not.
|
|
* @param {ASTNode} node The Comment node.
|
|
* @returns {boolean} True if the comment is at array end.
|
|
*/
|
|
function isCommentAtArrayEnd(node) {
|
|
return isCommentAtParentEnd(node, "ArrayExpression") || isCommentAtParentEnd(node, "ArrayPattern");
|
|
}
|
|
|
|
/**
|
|
* Checks if a comment node has lines around it (ignores inline comments)
|
|
* @param {ASTNode} node The Comment node.
|
|
* @param {Object} opts Options to determine the newline.
|
|
* @param {boolean} opts.after Should have a newline after this line.
|
|
* @param {boolean} opts.before Should have a newline before this line.
|
|
* @returns {void}
|
|
*/
|
|
function checkForEmptyLine(node, opts) {
|
|
let after = opts.after,
|
|
before = opts.before;
|
|
|
|
const prevLineNum = node.loc.start.line - 1,
|
|
nextLineNum = node.loc.end.line + 1,
|
|
commentIsNotAlone = codeAroundComment(node);
|
|
|
|
const blockStartAllowed = options.allowBlockStart && isCommentAtBlockStart(node),
|
|
blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(node),
|
|
objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(node),
|
|
objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(node),
|
|
arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(node),
|
|
arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(node);
|
|
|
|
const exceptionStartAllowed = blockStartAllowed || objectStartAllowed || arrayStartAllowed;
|
|
const exceptionEndAllowed = blockEndAllowed || objectEndAllowed || arrayEndAllowed;
|
|
|
|
// ignore top of the file and bottom of the file
|
|
if (prevLineNum < 1) {
|
|
before = false;
|
|
}
|
|
if (nextLineNum >= numLines) {
|
|
after = false;
|
|
}
|
|
|
|
// we ignore all inline comments
|
|
if (commentIsNotAlone) {
|
|
return;
|
|
}
|
|
|
|
const previousTokenOrComment = sourceCode.getTokenOrCommentBefore(node);
|
|
const nextTokenOrComment = sourceCode.getTokenOrCommentAfter(node);
|
|
|
|
// check for newline before
|
|
if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
|
|
!(isCommentNodeType(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, node))) {
|
|
const lineStart = node.range[0] - node.loc.start.column;
|
|
const range = [lineStart, lineStart];
|
|
|
|
context.report({
|
|
node,
|
|
message: "Expected line before comment.",
|
|
fix(fixer) {
|
|
return fixer.insertTextBeforeRange(range, "\n");
|
|
}
|
|
});
|
|
}
|
|
|
|
// check for newline after
|
|
if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
|
|
!(isCommentNodeType(nextTokenOrComment) && astUtils.isTokenOnSameLine(node, nextTokenOrComment))) {
|
|
context.report({
|
|
node,
|
|
message: "Expected line after comment.",
|
|
fix(fixer) {
|
|
return fixer.insertTextAfter(node, "\n");
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Public
|
|
//--------------------------------------------------------------------------
|
|
|
|
return {
|
|
|
|
LineComment(node) {
|
|
if (options.beforeLineComment || options.afterLineComment) {
|
|
checkForEmptyLine(node, {
|
|
after: options.afterLineComment,
|
|
before: options.beforeLineComment
|
|
});
|
|
}
|
|
},
|
|
|
|
BlockComment(node) {
|
|
if (options.beforeBlockComment || options.afterBlockComment) {
|
|
checkForEmptyLine(node, {
|
|
after: options.afterBlockComment,
|
|
before: options.beforeBlockComment
|
|
});
|
|
}
|
|
}
|
|
|
|
};
|
|
}
|
|
};
|
|
|