first commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
import header from "./src/rules/header.js";
|
||||
|
||||
export default {
|
||||
rules: { header },
|
||||
rulesConfig: { header: 0 },
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@ufb/eslint-plugin-header",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./index.js"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export function commentParser(text) {
|
||||
text = text.trim();
|
||||
|
||||
if (text.substr(0, 2) === '//') {
|
||||
return [
|
||||
'line',
|
||||
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 (/* ... */)',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user