'use strict'; var utils = require('../utils.js'); var utils$1 = require('@typescript-eslint/utils'); require('eslint-visitor-keys'); require('espree'); require('estraverse'); function isLastTokenEndOfLine(token, line) { const positionInLine = token.loc.start.column; return positionInLine === line.length - 1; } function isCommentsEndOfLine(token, comments, line) { if (!comments) return false; if (comments.loc.end.line > token.loc.end.line) return true; const positionInLine = comments.loc.end.column; return positionInLine === line.length; } function makeFixFunction({ optsNone, optsSemi, lastToken, commentsAfterLastToken, missingDelimiter, lastTokenLine, isSingleLine }) { if (optsNone && !isLastTokenEndOfLine(lastToken, lastTokenLine) && !isCommentsEndOfLine(lastToken, commentsAfterLastToken, lastTokenLine) && !isSingleLine) { return null; } return (fixer) => { if (optsNone) { return fixer.remove(lastToken); } const token = optsSemi ? ";" : ","; if (missingDelimiter) { return fixer.insertTextAfter(lastToken, token); } return fixer.replaceText(lastToken, token); }; } const BASE_SCHEMA = { type: "object", properties: { multiline: { type: "object", properties: { delimiter: { $ref: "#/items/0/$defs/multiLineOption" }, requireLast: { type: "boolean" } }, additionalProperties: false }, singleline: { type: "object", properties: { delimiter: { $ref: "#/items/0/$defs/singleLineOption" }, requireLast: { type: "boolean" } }, additionalProperties: false } }, additionalProperties: false }; var memberDelimiterStyle = utils.createRule({ name: "member-delimiter-style", package: "ts", meta: { type: "layout", docs: { description: "Require a specific member delimiter style for interfaces and type literals" }, fixable: "whitespace", messages: { unexpectedComma: "Unexpected separator (,).", unexpectedSemi: "Unexpected separator (;).", expectedComma: "Expected a comma.", expectedSemi: "Expected a semicolon." }, schema: [ { $defs: { multiLineOption: { type: "string", enum: ["none", "semi", "comma"] }, // note can't have "none" for single line delimiter as it's invalid syntax singleLineOption: { type: "string", enum: ["semi", "comma"] }, // note - need to define this last as it references the enums delimiterConfig: BASE_SCHEMA }, type: "object", properties: { ...BASE_SCHEMA.properties, overrides: { type: "object", properties: { interface: { $ref: "#/items/0/$defs/delimiterConfig" }, typeLiteral: { $ref: "#/items/0/$defs/delimiterConfig" } }, additionalProperties: false }, multilineDetection: { type: "string", enum: ["brackets", "last-member"] } }, additionalProperties: false } ] }, defaultOptions: [ { multiline: { delimiter: "semi", requireLast: true }, singleline: { delimiter: "semi", requireLast: false }, multilineDetection: "brackets" } ], create(context, [options]) { const sourceCode = context.sourceCode; const baseOptions = options; const overrides = baseOptions.overrides ?? {}; const interfaceOptions = utils.deepMerge( baseOptions, overrides.interface ); const typeLiteralOptions = utils.deepMerge( baseOptions, overrides.typeLiteral ); function checkLastToken(member, opts, isLast) { function getOption(type) { if (isLast && !opts.requireLast) { return type === "none"; } return opts.delimiter === type; } let messageId = null; let missingDelimiter = false; const lastToken = sourceCode.getLastToken(member, { includeComments: false }); if (!lastToken) return; const commentsAfterLastToken = sourceCode.getCommentsAfter(lastToken).pop(); const sourceCodeLines = sourceCode.getLines(); const lastTokenLine = sourceCodeLines[lastToken?.loc.start.line - 1]; const optsSemi = getOption("semi"); const optsComma = getOption("comma"); const optsNone = getOption("none"); if (lastToken.value === ";") { if (optsComma) { messageId = "expectedComma"; } else if (optsNone) { missingDelimiter = true; messageId = "unexpectedSemi"; } } else if (lastToken.value === ",") { if (optsSemi) { messageId = "expectedSemi"; } else if (optsNone) { missingDelimiter = true; messageId = "unexpectedComma"; } } else { if (optsSemi) { missingDelimiter = true; messageId = "expectedSemi"; } else if (optsComma) { missingDelimiter = true; messageId = "expectedComma"; } } if (messageId) { context.report({ node: lastToken, loc: { start: { line: lastToken.loc.end.line, column: lastToken.loc.end.column }, end: { line: lastToken.loc.end.line, column: lastToken.loc.end.column } }, messageId, fix: makeFixFunction({ optsNone, optsSemi, lastToken, commentsAfterLastToken, missingDelimiter, lastTokenLine, isSingleLine: opts.type === "single-line" }) }); } } function checkMemberSeparatorStyle(node) { const members = node.type === utils$1.AST_NODE_TYPES.TSInterfaceBody ? node.body : node.members; let isSingleLine = node.loc.start.line === node.loc.end.line; if (options.multilineDetection === "last-member" && !isSingleLine && members.length > 0) { const lastMember = members[members.length - 1]; if (lastMember.loc.end.line === node.loc.end.line) isSingleLine = true; } const typeOpts = node.type === utils$1.AST_NODE_TYPES.TSInterfaceBody ? interfaceOptions : typeLiteralOptions; const opts = isSingleLine ? { ...typeOpts.singleline, type: "single-line" } : { ...typeOpts.multiline, type: "multi-line" }; members.forEach((member, index) => { checkLastToken(member, opts ?? {}, index === members.length - 1); }); } return { TSInterfaceBody: checkMemberSeparatorStyle, TSTypeLiteral: checkMemberSeparatorStyle }; } }); module.exports = memberDelimiterStyle;