492 lines
14 KiB
JavaScript
492 lines
14 KiB
JavaScript
import fs from "fs";
|
|
import os from "os";
|
|
|
|
// @ts-ignore
|
|
function commentParser(text) {
|
|
text = text.trim();
|
|
|
|
if (text.substr(0, 2) === "//") {
|
|
return [
|
|
"line",
|
|
// @ts-ignore
|
|
text.split(/\r?\n/).map(function (line) {
|
|
return line.substr(2);
|
|
}),
|
|
];
|
|
} else if (text.substr(0, 2) === "/*" && text.substr(-2) === "*/") {
|
|
return ["block", text.substring(2, text.length - 2)];
|
|
} else {
|
|
throw new Error(
|
|
"Could not parse comment file: the file must contain either just line comments (//) or a single block comment (/* ... */)"
|
|
);
|
|
}
|
|
}
|
|
|
|
// @ts-ignore
|
|
function isPattern(object) {
|
|
return (
|
|
typeof object === "object" &&
|
|
Object.prototype.hasOwnProperty.call(object, "pattern")
|
|
);
|
|
}
|
|
|
|
// @ts-ignore
|
|
function match(actual, expected) {
|
|
if (expected.test) {
|
|
return expected.test(actual);
|
|
} else {
|
|
return expected === actual;
|
|
}
|
|
}
|
|
|
|
// @ts-ignore
|
|
function excludeShebangs(comments) {
|
|
// @ts-ignore
|
|
return comments.filter(function (comment) {
|
|
return comment.type !== "Shebang";
|
|
});
|
|
}
|
|
|
|
// Returns either the first block comment or the first set of line comments that
|
|
// are ONLY separated by a single newline. Note that this does not actually
|
|
// check if they are at the start of the file since that is already checked by
|
|
// hasHeader().
|
|
// @ts-ignore
|
|
function getLeadingComments(context, node) {
|
|
var all = excludeShebangs(
|
|
context
|
|
.getSourceCode()
|
|
.getAllComments(node.body.length ? node.body[0] : node)
|
|
);
|
|
if (all[0].type.toLowerCase() === "block") {
|
|
return [all[0]];
|
|
}
|
|
for (var i = 1; i < all.length; ++i) {
|
|
var txt = context
|
|
.getSourceCode()
|
|
.getText()
|
|
.slice(all[i - 1].range[1], all[i].range[0]);
|
|
if (!txt.match(/^(\r\n|\r|\n)$/)) {
|
|
break;
|
|
}
|
|
}
|
|
return all.slice(0, i);
|
|
}
|
|
|
|
// @ts-ignore
|
|
function genCommentBody(commentType, textArray, eol, numNewlines) {
|
|
var eols = eol.repeat(numNewlines);
|
|
if (commentType === "block") {
|
|
return "/*" + textArray.join(eol) + "*/" + eols;
|
|
} else {
|
|
return "//" + textArray.join(eol + "//") + eols;
|
|
}
|
|
}
|
|
|
|
// @ts-ignore
|
|
function genCommentsRange(context, comments, eol) {
|
|
var start = comments[0].range[0];
|
|
var end = comments.slice(-1)[0].range[1];
|
|
if (context.getSourceCode().text[end] === eol) {
|
|
end += eol.length;
|
|
}
|
|
return [start, end];
|
|
}
|
|
|
|
// @ts-ignore
|
|
function genPrependFixer(commentType, node, headerLines, eol, numNewlines) {
|
|
// @ts-ignore
|
|
return function (fixer) {
|
|
return fixer.insertTextBefore(
|
|
node,
|
|
genCommentBody(commentType, headerLines, eol, numNewlines)
|
|
);
|
|
};
|
|
}
|
|
|
|
function genReplaceFixer(
|
|
// @ts-ignore
|
|
commentType,
|
|
// @ts-ignore
|
|
context,
|
|
// @ts-ignore
|
|
leadingComments,
|
|
// @ts-ignore
|
|
headerLines,
|
|
// @ts-ignore
|
|
eol,
|
|
// @ts-ignore
|
|
numNewlines
|
|
) {
|
|
// @ts-ignore
|
|
return function (fixer) {
|
|
return fixer.replaceTextRange(
|
|
genCommentsRange(context, leadingComments, eol),
|
|
genCommentBody(commentType, headerLines, eol, numNewlines)
|
|
);
|
|
};
|
|
}
|
|
|
|
// @ts-ignore
|
|
function findSettings(options) {
|
|
var lastOption = options.length > 0 ? options[options.length - 1] : null;
|
|
if (
|
|
typeof lastOption === "object" &&
|
|
!Array.isArray(lastOption) &&
|
|
lastOption !== null &&
|
|
!Object.prototype.hasOwnProperty.call(lastOption, "pattern")
|
|
) {
|
|
return lastOption;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// @ts-ignore
|
|
function getEOL(options) {
|
|
var settings = findSettings(options);
|
|
if (settings && settings.lineEndings === "unix") {
|
|
return "\n";
|
|
}
|
|
if (settings && settings.lineEndings === "windows") {
|
|
return "\r\n";
|
|
}
|
|
return os.EOL;
|
|
}
|
|
|
|
// @ts-ignore
|
|
function hasHeader(src) {
|
|
if (src.substr(0, 2) === "#!") {
|
|
var m = src.match(/(\r\n|\r|\n)/);
|
|
if (m) {
|
|
src = src.slice(m.index + m[0].length);
|
|
}
|
|
}
|
|
return src.substr(0, 2) === "/*" || src.substr(0, 2) === "//";
|
|
}
|
|
|
|
// @ts-ignore
|
|
function matchesLineEndings(src, num) {
|
|
for (var j = 0; j < num; ++j) {
|
|
var m = src.match(/^(\r\n|\r|\n)/);
|
|
if (m) {
|
|
src = src.slice(m.index + m[0].length);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export default {
|
|
meta: {
|
|
type: "layout",
|
|
fixable: "whitespace",
|
|
schema: {
|
|
$ref: "#/definitions/options",
|
|
definitions: {
|
|
commentType: {
|
|
type: "string",
|
|
enum: ["block", "line"],
|
|
},
|
|
line: {
|
|
anyOf: [
|
|
{
|
|
type: "string",
|
|
},
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
pattern: {
|
|
type: "string",
|
|
},
|
|
template: {
|
|
type: "string",
|
|
},
|
|
},
|
|
required: ["pattern"],
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
},
|
|
headerLines: {
|
|
anyOf: [
|
|
{
|
|
$ref: "#/definitions/line",
|
|
},
|
|
{
|
|
type: "array",
|
|
items: {
|
|
$ref: "#/definitions/line",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
numNewlines: {
|
|
type: "integer",
|
|
minimum: 0,
|
|
},
|
|
settings: {
|
|
type: "object",
|
|
properties: {
|
|
lineEndings: {
|
|
type: "string",
|
|
enum: ["unix", "windows"],
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
options: {
|
|
anyOf: [
|
|
{
|
|
type: "array",
|
|
minItems: 1,
|
|
maxItems: 2,
|
|
items: [{ type: "string" }, { $ref: "#/definitions/settings" }],
|
|
},
|
|
{
|
|
type: "array",
|
|
minItems: 2,
|
|
maxItems: 3,
|
|
items: [
|
|
{ $ref: "#/definitions/commentType" },
|
|
{ $ref: "#/definitions/headerLines" },
|
|
{ $ref: "#/definitions/settings" },
|
|
],
|
|
},
|
|
{
|
|
type: "array",
|
|
minItems: 3,
|
|
maxItems: 4,
|
|
items: [
|
|
{ $ref: "#/definitions/commentType" },
|
|
{ $ref: "#/definitions/headerLines" },
|
|
{ $ref: "#/definitions/numNewlines" },
|
|
{ $ref: "#/definitions/settings" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
// @ts-ignore
|
|
create: function (context) {
|
|
var options = context.options;
|
|
var numNewlines = options.length > 2 ? options[2] : 1;
|
|
var eol = getEOL(options);
|
|
|
|
// If just one option then read comment from file
|
|
if (
|
|
options.length === 1 ||
|
|
(options.length === 2 && findSettings(options))
|
|
) {
|
|
var text = fs.readFileSync(context.options[0], "utf8");
|
|
options = commentParser(text);
|
|
}
|
|
|
|
var commentType = options[0].toLowerCase();
|
|
// @ts-ignore
|
|
var headerLines,
|
|
// @ts-ignore
|
|
fixLines = [];
|
|
// If any of the lines are regular expressions, then we can't
|
|
// automatically fix them. We set this to true below once we
|
|
// ensure none of the lines are of type RegExp
|
|
var canFix = false;
|
|
if (Array.isArray(options[1])) {
|
|
canFix = true;
|
|
headerLines = options[1].map(function (line) {
|
|
var isRegex = isPattern(line);
|
|
// Can only fix regex option if a template is also provided
|
|
if (isRegex && !line.template) {
|
|
canFix = false;
|
|
}
|
|
fixLines.push(line.template || line);
|
|
return isRegex ? new RegExp(line.pattern) : line;
|
|
});
|
|
} else if (isPattern(options[1])) {
|
|
var line = options[1];
|
|
headerLines = [new RegExp(line.pattern)];
|
|
fixLines.push(line.template || line);
|
|
// Same as above for regex and template
|
|
canFix = !!line.template;
|
|
} else {
|
|
canFix = true;
|
|
headerLines = options[1].split(/\r?\n/);
|
|
fixLines = headerLines;
|
|
}
|
|
|
|
return {
|
|
// @ts-ignore
|
|
Program: function (node) {
|
|
if (!hasHeader(context.getSourceCode().getText())) {
|
|
context.report({
|
|
loc: node.loc,
|
|
message: "missing header",
|
|
// @ts-ignore
|
|
fix: genPrependFixer(commentType, node, fixLines, eol, numNewlines),
|
|
});
|
|
} else {
|
|
var leadingComments = getLeadingComments(context, node);
|
|
|
|
if (!leadingComments.length) {
|
|
context.report({
|
|
loc: node.loc,
|
|
message: "missing header",
|
|
fix: canFix
|
|
? // @ts-ignore
|
|
genPrependFixer(commentType, node, fixLines, eol, numNewlines)
|
|
: null,
|
|
});
|
|
} else if (leadingComments[0].type.toLowerCase() !== commentType) {
|
|
context.report({
|
|
loc: node.loc,
|
|
message: "header should be a {{commentType}} comment",
|
|
data: {
|
|
commentType: commentType,
|
|
},
|
|
fix: canFix
|
|
? genReplaceFixer(
|
|
commentType,
|
|
context,
|
|
leadingComments,
|
|
// @ts-ignore
|
|
fixLines,
|
|
eol,
|
|
numNewlines
|
|
)
|
|
: null,
|
|
});
|
|
} else {
|
|
if (commentType === "line") {
|
|
if (leadingComments.length < headerLines.length) {
|
|
context.report({
|
|
loc: node.loc,
|
|
message: "incorrect header",
|
|
fix: canFix
|
|
? genReplaceFixer(
|
|
commentType,
|
|
context,
|
|
leadingComments,
|
|
// @ts-ignore
|
|
fixLines,
|
|
eol,
|
|
numNewlines
|
|
)
|
|
: null,
|
|
});
|
|
return;
|
|
}
|
|
for (var i = 0; i < headerLines.length; i++) {
|
|
// @ts-ignore
|
|
if (!match(leadingComments[i].value, headerLines[i])) {
|
|
context.report({
|
|
loc: node.loc,
|
|
message: "incorrect header",
|
|
fix: canFix
|
|
? genReplaceFixer(
|
|
commentType,
|
|
context,
|
|
leadingComments,
|
|
// @ts-ignore
|
|
fixLines,
|
|
eol,
|
|
numNewlines
|
|
)
|
|
: null,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
var postLineHeader = context
|
|
.getSourceCode()
|
|
.text.substr(
|
|
leadingComments[headerLines.length - 1].range[1],
|
|
numNewlines * 2
|
|
);
|
|
if (!matchesLineEndings(postLineHeader, numNewlines)) {
|
|
context.report({
|
|
loc: node.loc,
|
|
message: "no newline after header",
|
|
fix: canFix
|
|
? genReplaceFixer(
|
|
commentType,
|
|
context,
|
|
leadingComments,
|
|
// @ts-ignore
|
|
fixLines,
|
|
eol,
|
|
numNewlines
|
|
)
|
|
: null,
|
|
});
|
|
}
|
|
} else {
|
|
// if block comment pattern has more than 1 line, we also split the comment
|
|
var leadingLines = [leadingComments[0].value];
|
|
if (headerLines.length > 1) {
|
|
leadingLines = leadingComments[0].value.split(/\r?\n/);
|
|
}
|
|
|
|
var hasError = false;
|
|
if (leadingLines.length > headerLines.length) {
|
|
hasError = true;
|
|
}
|
|
for (i = 0; !hasError && i < headerLines.length; i++) {
|
|
// @ts-ignore
|
|
if (!match(leadingLines[i], headerLines[i])) {
|
|
hasError = true;
|
|
}
|
|
}
|
|
|
|
if (hasError) {
|
|
if (canFix && headerLines.length > 1) {
|
|
// @ts-ignore
|
|
fixLines = [fixLines.join(eol)];
|
|
}
|
|
context.report({
|
|
loc: node.loc,
|
|
message: "incorrect header",
|
|
fix: canFix
|
|
? genReplaceFixer(
|
|
commentType,
|
|
context,
|
|
leadingComments,
|
|
// @ts-ignore
|
|
fixLines,
|
|
eol,
|
|
numNewlines
|
|
)
|
|
: null,
|
|
});
|
|
} else {
|
|
var postBlockHeader = context
|
|
.getSourceCode()
|
|
.text.substr(leadingComments[0].range[1], numNewlines * 2);
|
|
if (!matchesLineEndings(postBlockHeader, numNewlines)) {
|
|
context.report({
|
|
loc: node.loc,
|
|
message: "no newline after header",
|
|
fix: canFix
|
|
? genReplaceFixer(
|
|
commentType,
|
|
context,
|
|
leadingComments,
|
|
// @ts-ignore
|
|
fixLines,
|
|
eol,
|
|
numNewlines
|
|
)
|
|
: null,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|