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

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