UNPKG

@stylistic/eslint-plugin-js

Version:

JavaScript stylistic rules for ESLint, migrated from [eslint core](https://github.com/eslint/eslint).

507 lines (504 loc) 17.5 kB
import { c as createRule, x as isColonToken, C as getStaticPropertyName, D as getStringLength, v as isOpeningBraceToken, k as isClosingBraceToken, L as LINEBREAK_MATCHER } from '../utils.js'; import 'eslint-visitor-keys'; import 'espree'; const listeningNodes = [ "ObjectExpression", "ObjectPattern", "ImportDeclaration", "ExportNamedDeclaration", "ExportAllDeclaration", "TSTypeLiteral", "TSInterfaceBody", "ClassBody" ]; function containsLineTerminator(str) { return LINEBREAK_MATCHER.test(str); } function last(arr) { return arr[arr.length - 1]; } function isSingleLine(node) { return node.loc.end.line === node.loc.start.line; } function isSingleLineImportAttributes(node, sourceCode) { const openingBrace = sourceCode.getTokenBefore(node.attributes[0], isOpeningBraceToken); const closingBrace = sourceCode.getTokenAfter(node.attributes[node.attributes.length - 1], isClosingBraceToken); return closingBrace.loc.end.line === openingBrace.loc.start.line; } function isSingleLineProperties(properties) { const [firstProp] = properties; const lastProp = last(properties); return firstProp.loc.start.line === lastProp.loc.end.line; } function initOptionProperty(toOptions, fromOptions) { toOptions.mode = fromOptions.mode || "strict"; if (typeof fromOptions.beforeColon !== "undefined") toOptions.beforeColon = +fromOptions.beforeColon; else toOptions.beforeColon = 0; if (typeof fromOptions.afterColon !== "undefined") toOptions.afterColon = +fromOptions.afterColon; else toOptions.afterColon = 1; if (typeof fromOptions.align !== "undefined") { if (typeof fromOptions.align === "object") { toOptions.align = fromOptions.align; } else { toOptions.align = { on: fromOptions.align, mode: toOptions.mode, beforeColon: toOptions.beforeColon, afterColon: toOptions.afterColon }; } } return toOptions; } function initOptions(toOptions, fromOptions) { if (typeof fromOptions.align === "object") { toOptions.align = initOptionProperty({}, fromOptions.align); toOptions.align.on = fromOptions.align.on || "colon"; toOptions.align.mode = fromOptions.align.mode || "strict"; toOptions.multiLine = initOptionProperty({}, fromOptions.multiLine || fromOptions); toOptions.singleLine = initOptionProperty({}, fromOptions.singleLine || fromOptions); } else { toOptions.multiLine = initOptionProperty({}, fromOptions.multiLine || fromOptions); toOptions.singleLine = initOptionProperty({}, fromOptions.singleLine || fromOptions); if (toOptions.multiLine.align) { toOptions.align = { on: toOptions.multiLine.align.on, mode: toOptions.multiLine.align.mode || toOptions.multiLine.mode, beforeColon: toOptions.multiLine.align.beforeColon, afterColon: toOptions.multiLine.align.afterColon }; } } toOptions.ignoredNodes = fromOptions.ignoredNodes || []; return toOptions; } var keySpacing = createRule({ name: "key-spacing", package: "js", meta: { type: "layout", docs: { description: "Enforce consistent spacing between keys and values in object literal properties" }, fixable: "whitespace", schema: [{ anyOf: [ { type: "object", properties: { align: { anyOf: [ { type: "string", enum: ["colon", "value"] }, { type: "object", properties: { mode: { type: "string", enum: ["strict", "minimum"] }, on: { type: "string", enum: ["colon", "value"] }, beforeColon: { type: "boolean" }, afterColon: { type: "boolean" } }, additionalProperties: false } ] }, mode: { type: "string", enum: ["strict", "minimum"] }, beforeColon: { type: "boolean" }, afterColon: { type: "boolean" }, ignoredNodes: { type: "array", items: { type: "string", enum: listeningNodes } } }, additionalProperties: false }, { type: "object", properties: { singleLine: { type: "object", properties: { mode: { type: "string", enum: ["strict", "minimum"] }, beforeColon: { type: "boolean" }, afterColon: { type: "boolean" } }, additionalProperties: false }, multiLine: { type: "object", properties: { align: { anyOf: [ { type: "string", enum: ["colon", "value"] }, { type: "object", properties: { mode: { type: "string", enum: ["strict", "minimum"] }, on: { type: "string", enum: ["colon", "value"] }, beforeColon: { type: "boolean" }, afterColon: { type: "boolean" } }, additionalProperties: false } ] }, mode: { type: "string", enum: ["strict", "minimum"] }, beforeColon: { type: "boolean" }, afterColon: { type: "boolean" } }, additionalProperties: false } }, additionalProperties: false }, { type: "object", properties: { singleLine: { type: "object", properties: { mode: { type: "string", enum: ["strict", "minimum"] }, beforeColon: { type: "boolean" }, afterColon: { type: "boolean" } }, additionalProperties: false }, multiLine: { type: "object", properties: { mode: { type: "string", enum: ["strict", "minimum"] }, beforeColon: { type: "boolean" }, afterColon: { type: "boolean" } }, additionalProperties: false }, align: { type: "object", properties: { mode: { type: "string", enum: ["strict", "minimum"] }, on: { type: "string", enum: ["colon", "value"] }, beforeColon: { type: "boolean" }, afterColon: { type: "boolean" } }, additionalProperties: false } }, additionalProperties: false } ] }], messages: { extraKey: "Extra space after {{computed}}key '{{key}}'.", extraValue: "Extra space before value for {{computed}}key '{{key}}'.", missingKey: "Missing space after {{computed}}key '{{key}}'.", missingValue: "Missing space before value for {{computed}}key '{{key}}'." } }, create(context) { const options = context.options[0] || {}; const ruleOptions = initOptions({}, options); const multiLineOptions = ruleOptions.multiLine; const singleLineOptions = ruleOptions.singleLine; const alignmentOptions = ruleOptions.align || null; const ignoredNodes = ruleOptions.ignoredNodes; const sourceCode = context.sourceCode; function isKeyValueProperty(property) { if (property.type === "ImportAttribute") return true; return !("method" in property && property.method || "shorthand" in property && property.shorthand || "kind" in property && property.kind !== "init" || property.type !== "Property"); } function getNextColon(node) { return sourceCode.getTokenAfter(node, isColonToken); } function getLastTokenBeforeColon(node) { const colonToken = getNextColon(node); return sourceCode.getTokenBefore(colonToken); } function getFirstTokenAfterColon(node) { const colonToken = getNextColon(node); return sourceCode.getTokenAfter(colonToken); } function continuesPropertyGroup(lastMember, candidate) { const groupEndLine = lastMember.loc.start.line; const candidateValueStartLine = (isKeyValueProperty(candidate) ? getFirstTokenAfterColon(candidate.key) : candidate).loc.start.line; if (candidateValueStartLine - groupEndLine <= 1) return true; const leadingComments = sourceCode.getCommentsBefore(candidate); if (leadingComments.length && leadingComments[0].loc.start.line - groupEndLine <= 1 && candidateValueStartLine - last(leadingComments).loc.end.line <= 1) { for (let i = 1; i < leadingComments.length; i++) { if (leadingComments[i].loc.start.line - leadingComments[i - 1].loc.end.line > 1) return false; } return true; } return false; } function getKey(property) { const key = property.key; if (property.type !== "ImportAttribute" && property.computed) return sourceCode.getText().slice(key.range[0], key.range[1]); return getStaticPropertyName(property); } function report(property, side, whitespace, expected, mode) { const diff = whitespace.length - expected; if ((diff && mode === "strict" || diff < 0 && mode === "minimum" || diff > 0 && !expected && mode === "minimum") && !(expected && containsLineTerminator(whitespace))) { const nextColon = getNextColon(property.key); const tokenBeforeColon = sourceCode.getTokenBefore(nextColon, { includeComments: true }); const tokenAfterColon = sourceCode.getTokenAfter(nextColon, { includeComments: true }); const isKeySide = side === "key"; const isExtra = diff > 0; const diffAbs = Math.abs(diff); const spaces = new Array(diffAbs + 1).join(" "); const locStart = isKeySide ? tokenBeforeColon.loc.end : nextColon.loc.start; const locEnd = isKeySide ? nextColon.loc.start : tokenAfterColon.loc.start; const missingLoc = isKeySide ? tokenBeforeColon.loc : tokenAfterColon.loc; const loc = isExtra ? { start: locStart, end: locEnd } : missingLoc; let fix; if (isExtra) { let range; if (isKeySide) range = [tokenBeforeColon.range[1], tokenBeforeColon.range[1] + diffAbs]; else range = [tokenAfterColon.range[0] - diffAbs, tokenAfterColon.range[0]]; fix = function(fixer) { return fixer.removeRange(range); }; } else { if (isKeySide) { fix = function(fixer) { return fixer.insertTextAfter(tokenBeforeColon, spaces); }; } else { fix = function(fixer) { return fixer.insertTextBefore(tokenAfterColon, spaces); }; } } let messageId; if (isExtra) messageId = side === "key" ? "extraKey" : "extraValue"; else messageId = side === "key" ? "missingKey" : "missingValue"; context.report({ node: property[side], loc, messageId, data: { computed: property.type !== "ImportAttribute" && property.computed ? "computed " : "", key: getKey(property) }, fix }); } } function getKeyWidth(property) { const startToken = sourceCode.getFirstToken(property); const endToken = getLastTokenBeforeColon(property.key); return getStringLength(sourceCode.getText().slice(startToken.range[0], endToken.range[1])); } function getPropertyWhitespace(property) { const whitespace = /(\s*):(\s*)/u.exec(sourceCode.getText().slice( property.key.range[1], property.value.range[0] )); if (whitespace) { return { beforeColon: whitespace[1], afterColon: whitespace[2] }; } return null; } function createGroups(properties) { if (properties.length === 1) return [properties]; return properties.reduce((groups, property) => { const currentGroup = last(groups); const prev = last(currentGroup); if (!prev || continuesPropertyGroup(prev, property)) currentGroup.push(property); else groups.push([property]); return groups; }, [ [] ]); } function verifyGroupAlignment(properties) { const length = properties.length; const widths = properties.map(getKeyWidth); const align = alignmentOptions.on; let targetWidth = Math.max(...widths); let beforeColon; let afterColon; let mode; if (alignmentOptions && length > 1) { beforeColon = alignmentOptions.beforeColon; afterColon = alignmentOptions.afterColon; mode = alignmentOptions.mode; } else { beforeColon = multiLineOptions.beforeColon; afterColon = multiLineOptions.afterColon; mode = alignmentOptions.mode; } targetWidth += align === "colon" ? beforeColon : afterColon; for (let i = 0; i < length; i++) { const property = properties[i]; const whitespace = getPropertyWhitespace(property); if (whitespace) { const width = widths[i]; if (align === "value") { report(property, "key", whitespace.beforeColon, beforeColon, mode); report(property, "value", whitespace.afterColon, targetWidth - width, mode); } else { report(property, "key", whitespace.beforeColon, targetWidth - width, mode); report(property, "value", whitespace.afterColon, afterColon, mode); } } } } function verifySpacing(node, lineOptions) { if (ignoredNodes.includes(node.parent.type)) return; const actual = getPropertyWhitespace(node); if (actual) { report(node, "key", actual.beforeColon, lineOptions.beforeColon, lineOptions.mode); report(node, "value", actual.afterColon, lineOptions.afterColon, lineOptions.mode); } } function verifyListSpacing(properties, lineOptions) { const length = properties.length; for (let i = 0; i < length; i++) verifySpacing(properties[i], lineOptions); } function verifyAlignment(properties) { createGroups(properties).forEach((group) => { const properties2 = group.filter(isKeyValueProperty); if (properties2.length > 0 && isSingleLineProperties(properties2)) verifyListSpacing(properties2, multiLineOptions); else verifyGroupAlignment(properties2); }); } function verifyImportAttributes(node) { if (ignoredNodes.includes(node.type)) return; if (!node.attributes) return; if (!node.attributes.length) return; if (isSingleLineImportAttributes(node, sourceCode)) verifyListSpacing(node.attributes, singleLineOptions); else verifyAlignment(node.attributes); } if (alignmentOptions) { return { ObjectExpression(node) { if (ignoredNodes.includes(node.type)) return; if (isSingleLine(node)) verifyListSpacing(node.properties.filter(isKeyValueProperty), singleLineOptions); else verifyAlignment(node.properties); }, ImportDeclaration(node) { verifyImportAttributes(node); }, ExportNamedDeclaration(node) { verifyImportAttributes(node); }, ExportAllDeclaration(node) { verifyImportAttributes(node); } }; } return { Property(node) { verifySpacing(node, isSingleLine(node.parent) ? singleLineOptions : multiLineOptions); }, ImportAttribute(node) { const parent = node.parent; verifySpacing(node, isSingleLineImportAttributes(parent, sourceCode) ? singleLineOptions : multiLineOptions); } }; } }); export { keySpacing as default };