UNPKG

@html-eslint/eslint-plugin

Version:
165 lines (152 loc) 4.95 kB
/** * @typedef { import("../types").RuleFixer } RuleFixer * * @typedef {Object} MessageId * @property {"closeStyleWrong"} CLOSE_STYLE_WRONG * @property {"newlineMissing"} NEWLINE_MISSING * @property {"newlineUnexpected"} NEWLINE_UNEXPECTED * * @typedef {Object} Option * @property {"sameline" | "newline"} [option.closeStyle] * @property {number} [options.ifAttrsMoreThan] * * @typedef { import("../types").RuleModule<[Option]> } RuleModule */ const { RULE_CATEGORY } = require("../constants"); const { createVisitors } = require("./utils/visitors"); /** * @type {MessageId} */ const MESSAGE_ID = { CLOSE_STYLE_WRONG: "closeStyleWrong", NEWLINE_MISSING: "newlineMissing", NEWLINE_UNEXPECTED: "newlineUnexpected", }; /** * @type {RuleModule} */ module.exports = { meta: { type: "code", docs: { description: "Enforce newline between attributes", category: RULE_CATEGORY.STYLE, recommended: true, }, fixable: true, schema: [ { type: "object", properties: { closeStyle: { enum: ["newline", "sameline"], }, ifAttrsMoreThan: { type: "integer", }, }, }, ], messages: { [MESSAGE_ID.CLOSE_STYLE_WRONG]: "Closing bracket was on {{actual}}; expected {{expected}}", [MESSAGE_ID.NEWLINE_MISSING]: "Newline expected before {{attrName}}", [MESSAGE_ID.NEWLINE_UNEXPECTED]: "Newlines not expected between attributes, since this tag has fewer than {{attrMin}} attributes", }, }, create(context) { const options = context.options[0] || {}; const attrMin = typeof options.ifAttrsMoreThan !== "number" ? 2 : options.ifAttrsMoreThan; const closeStyle = options.closeStyle || "newline"; return createVisitors(context, { Tag(node) { const shouldBeMultiline = node.attributes.length > attrMin; /** * This doesn't do any indentation, so the result will look silly. Indentation should be covered by the `indent` rule * @param {RuleFixer} fixer */ function fix(fixer) { const spacer = shouldBeMultiline ? "\n" : " "; let expected = node.openStart.value; for (const attr of node.attributes) { expected += `${spacer}${attr.key.value}`; if (attr.startWrapper && attr.value && attr.endWrapper) { expected += `=${attr.startWrapper.value}${attr.value.value}${attr.endWrapper.value}`; } } if (shouldBeMultiline && closeStyle === "newline") { expected += "\n"; } else if (node.selfClosing) { expected += " "; } expected += node.openEnd.value; return fixer.replaceTextRange( [node.openStart.range[0], node.openEnd.range[1]], expected ); } if (shouldBeMultiline) { let index = 0; for (const attr of node.attributes) { const attrPrevious = node.attributes[index - 1]; const relativeToNode = attrPrevious || node.openStart; if (attr.loc.start.line === relativeToNode.loc.end.line) { return context.report({ node, data: { attrName: attr.key.value, }, fix, messageId: MESSAGE_ID.NEWLINE_MISSING, }); } index += 1; } const attrLast = node.attributes[node.attributes.length - 1]; const closeStyleActual = node.openEnd.loc.start.line === attrLast.loc.end.line ? "sameline" : "newline"; if (closeStyle !== closeStyleActual) { return context.report({ node, data: { actual: closeStyleActual, expected: closeStyle, }, fix, messageId: MESSAGE_ID.CLOSE_STYLE_WRONG, }); } } else { let expectedLastLineNum = node.openStart.loc.start.line; for (const attr of node.attributes) { if (shouldBeMultiline) { expectedLastLineNum += 1; } if (attr.value) { const valueLineSpan = attr.value.loc.end.line - attr.value.loc.start.line; expectedLastLineNum += valueLineSpan; } } if (shouldBeMultiline && closeStyle === "newline") { expectedLastLineNum += 1; } if (node.openEnd.loc.end.line !== expectedLastLineNum) { return context.report({ node, data: { attrMin: `${attrMin}`, }, fix, messageId: MESSAGE_ID.NEWLINE_UNEXPECTED, }); } } }, }); }, };