@blitz/eslint-plugin
Version:
An ESLint config to enforce a consistent code styles across StackBlitz projects
244 lines • 12.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultOptions = exports.ruleName = void 0;
const util_1 = require("../util");
const messages_1 = require("../util/messages");
exports.ruleName = 'comment-syntax';
exports.defaultOptions = {
ignoredWords: [],
allowedParagraphEndings: ['.', ':', '`', ')', '}', ']', ';', '!', '?'],
};
const SPACE_CHARCODE = ' '.charCodeAt(0);
const SLASH_CHARCODE = '/'.charCodeAt(0);
const STAR = '*';
const BLOCK_COMMENT_END = /^\s*$/;
const EMPTY_BLOCK_COMMENT_LINE = /^\s*\*$/;
const CODE_BLOCK = /^\s*\*\s+```/;
const BLOCK_COMMENT_LINE_START = /^\s*\*\s+.*/;
const JS_DOC_REGEX = /^\s*\*\s*@.+?/;
const LIST_ITEM = /^\s*\*\s*(?:-|\d\.)/;
const LIST_ITEM_INDENTATION = /^\s*\*(\s*(?:-|\d[.:)])\s*)/;
const LINE_INFO = '\n\nLine: {{line}}';
function isCapital(char) {
return char != null && char === char.toUpperCase();
}
function isLetter(char) {
return /\w/.test(char);
}
function isCapitalizedOrAllowed(text, ignoredWords) {
let firstWord = '';
let isWordCapital = true;
for (const char of text) {
if (char?.charCodeAt(0) === SPACE_CHARCODE || !isLetter(char)) {
break;
}
if (isLetter(char)) {
firstWord += char;
}
if (!isCapital(char)) {
isWordCapital = false;
}
}
if (isWordCapital) {
return true;
}
if (ignoredWords.includes(firstWord)) {
return true;
}
return false;
}
const isRegion = (comment) => {
return comment.startsWith('#region') || comment.startsWith('#endregion');
};
exports.default = (0, util_1.createRule)({
name: exports.ruleName,
meta: {
type: 'layout',
docs: {
description: (0, messages_1.oneLine) `Enforce block comments to start with a capital first letter and end with a dot and
line comments to not start with a capital first letter and no dot
`,
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
ignoredWords: {
type: 'array',
uniqueItems: true,
description: 'Determines which words do not have to be capitalized',
default: exports.defaultOptions.ignoredWords,
},
allowedParagraphEndings: {
type: 'array',
uniqueItems: true,
description: 'Specifies the characters that can be used to end a paragraph',
default: exports.defaultOptions.allowedParagraphEndings,
},
},
additionalProperties: false,
},
],
messages: {
shouldStartWithSpace: 'Line comment should start with a space.',
shouldStartWithBlock: 'Block comment should start with `/**\\n *`.',
lineCommentCapital: 'Line comment cannot start with a capital letter unless the entire word is capitalized.',
lineCommentEnding: 'Line comment cannot end with a dot.',
paragraphCapitalized: `Paragraph should start with a capital letter.${LINE_INFO}`,
shouldEndWithDot: `Paragraph should end with a dot.${LINE_INFO}`,
shouldEndWithBlock: 'Block comment should end with `\\n*/`.',
noSpaceBeforeEnd: 'Block comment should not end with an empty line.',
invalidListItem: `List item requires a space at the beginning.${LINE_INFO}`,
invalidParagraphEnding: `Paragraph should end with one of {{allowedParagraphEndings}}.${LINE_INFO}`,
invalidBlockCommentLine: `Each line in a block comment requires a space after '*'.${LINE_INFO}`,
spaceBeforeJSDoc: `Requires newline before JSDocs.${LINE_INFO}`,
},
},
defaultOptions: [exports.defaultOptions],
create: (context, [options]) => {
return {
Program() {
const { ignoredWords, allowedParagraphEndings } = { ...exports.defaultOptions, ...options };
const { sourceCode } = context;
const comments = sourceCode.getAllComments();
for (const comment of comments) {
if (comment.type === 'Line') {
const firstChar = comment.value?.charCodeAt(0);
const secondChar = comment.value[1];
const lastChar = comment.value[comment.value.length - 1];
if (firstChar !== SPACE_CHARCODE && firstChar !== SLASH_CHARCODE && !isRegion(comment.value)) {
context.report({ node: comment, messageId: 'shouldStartWithSpace' });
// if this one fails, the others are interpreted incorrectly
continue;
}
if (isLetter(secondChar) &&
isCapital(secondChar) &&
!isCapitalizedOrAllowed(comment.value.slice(1), ignoredWords)) {
context.report({ node: comment, messageId: 'lineCommentCapital' });
}
if (lastChar === '.' && !comment.value.endsWith('etc.') && !comment.value.endsWith('...')) {
context.report({ node: comment, messageId: 'lineCommentEnding' });
}
continue;
}
if (comment.type === 'Block') {
let lines = comment.value.split('\n');
if (lines.length <= 1) {
// single line block comments are ignored
continue;
}
// verify the first char is a '*'
if (lines[0] !== STAR) {
context.report({ node: comment, messageId: 'shouldStartWithBlock' });
continue;
}
lines = lines.slice(1);
let newParagraph = true;
let insideCodeBlock = false;
let jsdocContinuation = false;
let prevListItemIndentation;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineData = { line: line.trim() };
const prevLine = lines[i - 1];
const nextLine = lines[i + 1];
const isLastContentLine = BLOCK_COMMENT_END.test(nextLine);
const isLastLine = i === lines.length - 1;
const isCurrentLineEmpty = EMPTY_BLOCK_COMMENT_LINE.test(line);
const isNextLineEmpty = EMPTY_BLOCK_COMMENT_LINE.test(nextLine);
const isCurrentLineJSDoc = JS_DOC_REGEX.test(line);
const isNextLineCodeBlock = CODE_BLOCK.test(nextLine);
const isListItem = LIST_ITEM.test(line);
const nextLineNotEmptyOrEnd = !isNextLineEmpty && !isLastContentLine;
if (isLastLine) {
if (!BLOCK_COMMENT_END.test(line)) {
context.report({ node: comment, messageId: 'shouldEndWithBlock' });
break;
}
if (EMPTY_BLOCK_COMMENT_LINE.test(prevLine)) {
context.report({ node: comment, messageId: 'noSpaceBeforeEnd' });
break;
}
continue;
}
if (CODE_BLOCK.test(line)) {
if (insideCodeBlock) {
insideCodeBlock = false;
newParagraph = true;
continue;
}
insideCodeBlock = true;
continue;
}
if (isCurrentLineJSDoc && !insideCodeBlock) {
if (prevLine && !EMPTY_BLOCK_COMMENT_LINE.test(prevLine) && !JS_DOC_REGEX.test(prevLine)) {
context.report({ node: comment, messageId: 'spaceBeforeJSDoc', data: { ...lineData } });
break;
}
jsdocContinuation = nextLineNotEmptyOrEnd && !JS_DOC_REGEX.test(nextLine);
}
if (insideCodeBlock || isCurrentLineEmpty || isCurrentLineJSDoc) {
continue;
}
if (!isCurrentLineEmpty && !BLOCK_COMMENT_LINE_START.test(line)) {
context.report({ node: comment, messageId: 'invalidBlockCommentLine', data: { ...lineData } });
break;
}
if (isListItem) {
const listItemText = line.replace(LIST_ITEM, '');
const spaces = listItemText.match(/^(\s+)/g)?.[0].length;
prevListItemIndentation = line.match(LIST_ITEM_INDENTATION)?.[1].length;
if (spaces !== 1) {
context.report({ node: comment, messageId: 'invalidListItem', data: { ...lineData } });
break;
}
continue;
}
/**
* If we saw a list item before we check if the current line has the same indentation.
* If not, we simply continue.
*/
if (prevListItemIndentation != null && prevListItemIndentation > 0) {
const currentLineIdentation = line.match(/^\s*\*(\s*)/)?.[1].length;
if (currentLineIdentation === prevListItemIndentation) {
continue;
}
else {
// indentation is different so we reset the state and continue with checking other rules
prevListItemIndentation = undefined;
}
}
if (newParagraph && !jsdocContinuation) {
const text = line.replace(/^\s*\*\s*/, '');
const firstChar = text[0];
if (isLetter(firstChar) && !(isCapital(firstChar) || isCapitalizedOrAllowed(text, ignoredWords))) {
context.report({ node: comment, messageId: 'paragraphCapitalized', data: { ...lineData } });
break;
}
}
jsdocContinuation = jsdocContinuation && nextLineNotEmptyOrEnd;
newParagraph = isNextLineEmpty && !isLastContentLine;
if (isNextLineEmpty || isLastContentLine || isNextLineCodeBlock) {
const lastChar = line[line.length - 1];
if (isLastContentLine && !allowedParagraphEndings.some((ending) => ending === lastChar)) {
context.report({ node: comment, messageId: 'shouldEndWithDot', data: { ...lineData } });
break;
}
if (!isLastContentLine && !allowedParagraphEndings.some((ending) => ending === lastChar)) {
context.report({
node: comment,
messageId: 'invalidParagraphEnding',
data: { ...lineData, allowedParagraphEndings: `[${allowedParagraphEndings.join(' ')}]` },
});
break;
}
}
}
}
}
},
};
},
});
//# sourceMappingURL=comment-syntax.js.map