padding-lines-in-component-definition.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. /**
  2. * @author ItMaga <https://github.com/ItMaga>
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. /**
  7. * @typedef {import('../utils').ComponentProp} ComponentProp
  8. * @typedef {import('../utils').ComponentEmit} ComponentEmit
  9. * @typedef {import('../utils').GroupName} GroupName
  10. */
  11. const utils = require('../utils')
  12. const { isCommentToken } = require('@eslint-community/eslint-utils')
  13. const AvailablePaddingOptions = {
  14. Never: 'never',
  15. Always: 'always',
  16. Ignore: 'ignore'
  17. }
  18. const OptionKeys = {
  19. BetweenOptions: 'betweenOptions',
  20. WithinOption: 'withinOption',
  21. BetweenItems: 'betweenItems',
  22. WithinEach: 'withinEach',
  23. GroupSingleLineProperties: 'groupSingleLineProperties'
  24. }
  25. /**
  26. * @param {Token} node
  27. */
  28. function isComma(node) {
  29. return node.type === 'Punctuator' && node.value === ','
  30. }
  31. /**
  32. * @typedef {Exclude<ComponentProp | ComponentEmit, {type:'infer-type'}> & { node: {type: 'Property' | 'SpreadElement'} }} ValidComponentPropOrEmit
  33. */
  34. /**
  35. * @template {ComponentProp | ComponentEmit} T
  36. * @param {T} propOrEmit
  37. * @returns {propOrEmit is ValidComponentPropOrEmit & T}
  38. */
  39. function isValidProperties(propOrEmit) {
  40. return Boolean(
  41. propOrEmit.type !== 'infer-type' &&
  42. propOrEmit.node &&
  43. ['Property', 'SpreadElement'].includes(propOrEmit.node.type)
  44. )
  45. }
  46. /**
  47. * Split the source code into multiple lines based on the line delimiters.
  48. * @param {string} text Source code as a string.
  49. * @returns {string[]} Array of source code lines.
  50. */
  51. function splitLines(text) {
  52. return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
  53. }
  54. /**
  55. * @param {any} initialOption
  56. * @param {string} optionKey
  57. * @private
  58. * */
  59. function parseOption(initialOption, optionKey) {
  60. return typeof initialOption === 'string'
  61. ? initialOption
  62. : initialOption[optionKey]
  63. }
  64. /**
  65. * @param {any} initialOption
  66. * @param {string} optionKey
  67. * @private
  68. * */
  69. function parseBooleanOption(initialOption, optionKey) {
  70. if (typeof initialOption === 'string') {
  71. if (initialOption === AvailablePaddingOptions.Always) return true
  72. if (initialOption === AvailablePaddingOptions.Never) return false
  73. }
  74. return initialOption[optionKey]
  75. }
  76. /**
  77. * @param {(Property | SpreadElement)} currentProperty
  78. * @param {(Property | SpreadElement)} nextProperty
  79. * @param {boolean} option
  80. * @returns {boolean}
  81. * @private
  82. * */
  83. function needGroupSingleLineProperties(currentProperty, nextProperty, option) {
  84. const isSingleCurrentProperty =
  85. currentProperty.loc.start.line === currentProperty.loc.end.line
  86. const isSingleNextProperty =
  87. nextProperty.loc.start.line === nextProperty.loc.end.line
  88. return isSingleCurrentProperty && isSingleNextProperty && option
  89. }
  90. module.exports = {
  91. meta: {
  92. type: 'layout',
  93. docs: {
  94. description: 'require or disallow padding lines in component definition',
  95. categories: undefined,
  96. url: 'https://eslint.vuejs.org/rules/padding-lines-in-component-definition.html'
  97. },
  98. fixable: 'whitespace',
  99. schema: [
  100. {
  101. oneOf: [
  102. {
  103. enum: [
  104. AvailablePaddingOptions.Always,
  105. AvailablePaddingOptions.Never
  106. ]
  107. },
  108. {
  109. type: 'object',
  110. additionalProperties: false,
  111. properties: {
  112. [OptionKeys.BetweenOptions]: {
  113. enum: Object.values(AvailablePaddingOptions)
  114. },
  115. [OptionKeys.WithinOption]: {
  116. oneOf: [
  117. {
  118. enum: Object.values(AvailablePaddingOptions)
  119. },
  120. {
  121. type: 'object',
  122. patternProperties: {
  123. '^[a-zA-Z]*$': {
  124. oneOf: [
  125. {
  126. enum: Object.values(AvailablePaddingOptions)
  127. },
  128. {
  129. type: 'object',
  130. properties: {
  131. [OptionKeys.BetweenItems]: {
  132. enum: Object.values(AvailablePaddingOptions)
  133. },
  134. [OptionKeys.WithinEach]: {
  135. enum: Object.values(AvailablePaddingOptions)
  136. }
  137. },
  138. additionalProperties: false
  139. }
  140. ]
  141. }
  142. },
  143. minProperties: 1,
  144. additionalProperties: false
  145. }
  146. ]
  147. },
  148. [OptionKeys.GroupSingleLineProperties]: {
  149. type: 'boolean'
  150. }
  151. }
  152. }
  153. ]
  154. }
  155. ],
  156. messages: {
  157. never: 'Unexpected blank line before this definition.',
  158. always: 'Expected blank line before this definition.',
  159. groupSingleLineProperties:
  160. 'Unexpected blank line between single line properties.'
  161. }
  162. },
  163. /** @param {RuleContext} context */
  164. create(context) {
  165. const options = context.options[0] || AvailablePaddingOptions.Always
  166. const sourceCode = context.getSourceCode()
  167. /**
  168. * @param {(Property | SpreadElement)} currentProperty
  169. * @param {(Property | SpreadElement | Token)} nextProperty
  170. * @param {RuleFixer} fixer
  171. * */
  172. function replaceLines(currentProperty, nextProperty, fixer) {
  173. const commaToken = sourceCode.getTokenAfter(currentProperty, isComma)
  174. const start = commaToken ? commaToken.range[1] : currentProperty.range[1]
  175. const end = nextProperty.range[0]
  176. const paddingText = sourceCode.text.slice(start, end)
  177. const newText = `\n${splitLines(paddingText).pop()}`
  178. return fixer.replaceTextRange([start, end], newText)
  179. }
  180. /**
  181. * @param {(Property | SpreadElement)} currentProperty
  182. * @param {(Property | SpreadElement | Token)} nextProperty
  183. * @param {RuleFixer} fixer
  184. * @param {number} betweenLinesRange
  185. * */
  186. function insertLines(
  187. currentProperty,
  188. nextProperty,
  189. fixer,
  190. betweenLinesRange
  191. ) {
  192. const commaToken = sourceCode.getTokenAfter(currentProperty, isComma)
  193. const lineBeforeNextProperty =
  194. sourceCode.lines[nextProperty.loc.start.line - 1]
  195. const lastSpaces = /** @type {RegExpExecArray} */ (
  196. /^\s*/.exec(lineBeforeNextProperty)
  197. )[0]
  198. const newText = betweenLinesRange === 0 ? `\n\n${lastSpaces}` : '\n'
  199. return fixer.insertTextAfter(commaToken || currentProperty, newText)
  200. }
  201. /**
  202. * @param {(Property | SpreadElement)[]} properties
  203. * @param {any} option
  204. * @param {any} nextOption
  205. * */
  206. function verify(properties, option, nextOption) {
  207. const groupSingleLineProperties = parseBooleanOption(
  208. options,
  209. OptionKeys.GroupSingleLineProperties
  210. )
  211. for (const [i, currentProperty] of properties.entries()) {
  212. const nextProperty = properties[i + 1]
  213. if (nextProperty && option !== AvailablePaddingOptions.Ignore) {
  214. const tokenBeforeNext = sourceCode.getTokenBefore(nextProperty, {
  215. includeComments: true
  216. })
  217. const isCommentBefore = isCommentToken(tokenBeforeNext)
  218. const reportNode = isCommentBefore ? tokenBeforeNext : nextProperty
  219. const betweenLinesRange =
  220. reportNode.loc.start.line - currentProperty.loc.end.line
  221. if (
  222. needGroupSingleLineProperties(
  223. currentProperty,
  224. nextProperty,
  225. groupSingleLineProperties
  226. )
  227. ) {
  228. if (betweenLinesRange > 1) {
  229. context.report({
  230. node: reportNode,
  231. messageId: 'groupSingleLineProperties',
  232. loc: reportNode.loc,
  233. fix(fixer) {
  234. return replaceLines(currentProperty, reportNode, fixer)
  235. }
  236. })
  237. }
  238. continue
  239. }
  240. if (
  241. betweenLinesRange <= 1 &&
  242. option === AvailablePaddingOptions.Always
  243. ) {
  244. context.report({
  245. node: reportNode,
  246. messageId: 'always',
  247. loc: reportNode.loc,
  248. fix(fixer) {
  249. return insertLines(
  250. currentProperty,
  251. reportNode,
  252. fixer,
  253. betweenLinesRange
  254. )
  255. }
  256. })
  257. } else if (
  258. betweenLinesRange > 1 &&
  259. option === AvailablePaddingOptions.Never
  260. ) {
  261. context.report({
  262. node: reportNode,
  263. messageId: 'never',
  264. loc: reportNode.loc,
  265. fix(fixer) {
  266. return replaceLines(currentProperty, reportNode, fixer)
  267. }
  268. })
  269. }
  270. }
  271. if (!nextOption) return
  272. const name = /** @type {GroupName | null} */ (
  273. currentProperty.type === 'Property' &&
  274. utils.getStaticPropertyName(currentProperty)
  275. )
  276. if (!name) continue
  277. const propertyOption = parseOption(nextOption, name)
  278. if (!propertyOption) continue
  279. const nestedProperties =
  280. currentProperty.type === 'Property' &&
  281. currentProperty.value.type === 'ObjectExpression' &&
  282. currentProperty.value.properties
  283. if (!nestedProperties) continue
  284. verify(
  285. nestedProperties,
  286. parseOption(propertyOption, OptionKeys.BetweenItems),
  287. parseOption(propertyOption, OptionKeys.WithinEach)
  288. )
  289. }
  290. }
  291. return utils.compositingVisitors(
  292. utils.defineVueVisitor(context, {
  293. onVueObjectEnter(node) {
  294. verify(
  295. node.properties,
  296. parseOption(options, OptionKeys.BetweenOptions),
  297. parseOption(options, OptionKeys.WithinOption)
  298. )
  299. }
  300. }),
  301. utils.defineScriptSetupVisitor(context, {
  302. onDefinePropsEnter(_, props) {
  303. const propNodes = props
  304. .filter(isValidProperties)
  305. .map((prop) => prop.node)
  306. const withinOption = parseOption(options, OptionKeys.WithinOption)
  307. const propsOption = withinOption && parseOption(withinOption, 'props')
  308. if (!propsOption) return
  309. verify(
  310. propNodes,
  311. parseOption(propsOption, OptionKeys.BetweenItems),
  312. parseOption(propsOption, OptionKeys.WithinEach)
  313. )
  314. },
  315. onDefineEmitsEnter(_, emits) {
  316. const emitNodes = emits
  317. .filter(isValidProperties)
  318. .map((emit) => emit.node)
  319. const withinOption = parseOption(options, OptionKeys.WithinOption)
  320. const emitsOption = withinOption && parseOption(withinOption, 'emits')
  321. if (!emitsOption) return
  322. verify(
  323. emitNodes,
  324. parseOption(emitsOption, OptionKeys.BetweenItems),
  325. parseOption(emitsOption, OptionKeys.WithinEach)
  326. )
  327. },
  328. onDefineOptionsEnter(node) {
  329. if (node.arguments.length === 0) return
  330. const define = node.arguments[0]
  331. if (define.type !== 'ObjectExpression') return
  332. verify(
  333. define.properties,
  334. parseOption(options, OptionKeys.BetweenOptions),
  335. parseOption(options, OptionKeys.WithinOption)
  336. )
  337. }
  338. })
  339. )
  340. }
  341. }