Repository: rrd108/vue-mess-detector
Pull request #92 (2024-08-03 18:15:44)

feat: add vue-recommended/element-attribute-order rule w tests


Patch:
diff --git a/.gitignore b/.gitignore
index 092a775..f6fc628 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 node_modules
 test
+.idea
 docs/.vitepress/cache
-docs/.vitepress/dist
\ No newline at end of file
+docs/.vitepress/dist
diff --git a/src/rules/rules.ts b/src/rules/rules.ts
index 3f894e7..7c75b1e 100644
--- a/src/rules/rules.ts
+++ b/src/rules/rules.ts
@@ -1,7 +1,7 @@
 export const RULES = {
   'vue-caution': ['implicitParentChildCommunication'],
   'vue-essential': ['globalStyle', 'simpleProp', 'singleNameComponent', 'vforNoKey', 'vifWithVfor'],
-  'vue-recommended': ['topLevelElementOrder'],
+  'vue-recommended': ['topLevelElementOrder', 'elementAttributeOrder'],
   'vue-strong': [
     'componentFilenameCasing',
     'componentFiles',
@@ -11,9 +11,9 @@ export const RULES = {
     'selfClosingComponents',
     'simpleComputed',
     'templateSimpleExpression',
-    'fullWordComponentName'
+    'fullWordComponentName',
   ],
-  rrd: [
+  'rrd': [
     'cyclomaticComplexity',
     'elseCondition',
     'functionSize',
diff --git a/src/rules/vue-recommended/elementAttributeOrder.ts b/src/rules/vue-recommended/elementAttributeOrder.ts
new file mode 100644
index 0000000..55d3790
--- /dev/null
+++ b/src/rules/vue-recommended/elementAttributeOrder.ts
@@ -0,0 +1,83 @@
+/* eslint-disable no-cond-assign */
+import type { SFCTemplateBlock } from '@vue/compiler-sfc'
+import { BG_ERR, BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'
+import { getUniqueFilenameCount } from '../../helpers'
+
+interface ElementAttributeOrder { filename: string, message: string }
+
+const elementAttributeOrderFiles: ElementAttributeOrder[] = []
+
+const ATTRIBUTE_ORDER = [
+  'is',
+  'v-for',
+  'v-if',
+  'v-else-if',
+  'v-else',
+  'v-show',
+  'v-cloak',
+  'v-pre',
+  'v-once',
+  'id',
+  'ref',
+  'key',
+  'v-model',
+  'v-on',
+  'v-html',
+  'v-text',
+]
+
+const checkElementAttributeOrder = (template: SFCTemplateBlock | null, filePath: string) => {
+  if (!template)
+    return
+
+  // Remove the <template> tags to avoid checking them
+  const innerTemplate = template.content.replace(/<\/?template>/g, '')
+
+  const tagRegex = /<(\w+)(\s[^>]+)?>/g
+  const attributeRegex = /(\w+(?:-\w+)*)(?:="[^"]*")?/g
+
+  let match
+  while ((match = tagRegex.exec(innerTemplate)) !== null) {
+    const tagName = match[1]
+    const attributeString = match[2]
+
+    if (attributeString) {
+      // Extract attribute names from the attribute string
+      const attributes = Array.from(attributeString.matchAll(attributeRegex), attr => attr[1])
+
+      // Filter attributes to only includes those that are in the ATTRIBUTE_ORDER constant array
+      const filteredAttrs = attributes.filter(item => ATTRIBUTE_ORDER.includes((item)))
+
+      let lastIdx = -1
+      for (const attr of filteredAttrs) {
+        const currIdx = ATTRIBUTE_ORDER.indexOf(attr)
+        if (currIdx !== -1 && currIdx < lastIdx) {
+          elementAttributeOrderFiles.push({
+            filename: filePath,
+            message: `${filePath} tag has attributes out of order ${BG_WARN}(${tagName})${BG_RESET}`,
+          })
+          break
+        }
+        lastIdx = currIdx
+      }
+    }
+  }
+}
+
+const reportElementAttributeOrder = () => {
+  if (elementAttributeOrderFiles.length > 0) {
+    // count only non duplicated objects (by its `filename` property)
+    const fileCount = getUniqueFilenameCount<ElementAttributeOrder>(elementAttributeOrderFiles, 'filename')
+
+    console.log(`\n${TEXT_INFO}vue-recommended${TEXT_RESET} ${BG_ERR}element attribute order ${BG_RESET} detected in ${fileCount} files.`)
+    console.log(
+            `👉 ${TEXT_WARN}The attributes of elements (including components) should be ordered consistently.${TEXT_RESET} See: https://vuejs.org/style-guide/rules-recommended.html#element-attribute-order`,
+    )
+    elementAttributeOrderFiles.forEach((file) => {
+      console.log(`- ${file.message} 🚨`)
+    })
+  }
+  return elementAttributeOrderFiles.length
+}
+
+export { checkElementAttributeOrder, reportElementAttributeOrder }
diff --git a/src/rulesCheck.ts b/src/rulesCheck.ts
index 41f4b00..07c884c 100644
--- a/src/rulesCheck.ts
+++ b/src/rulesCheck.ts
@@ -16,6 +16,7 @@ import { checkSelfClosingComponents } from './rules/vue-strong/selfClosingCompon
 import { checkDirectiveShorthands } from './rules/vue-strong/directiveShorthands'
 import { checkFullWordComponentName } from './rules/vue-strong/fullWordComponentName'
 import { checkTopLevelElementOrder } from './rules/vue-recommended/topLevelElementOrder'
+import { checkElementAttributeOrder } from "./rules/vue-recommended/elementAttributeOrder"
 import { checkTooManyProps } from './rules/rrd/tooManyProps'
 import { checkFunctionSize } from './rules/rrd/functionSize'
 import { checkParameterCount } from './rules/rrd/parameterCount'
@@ -51,6 +52,7 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: A
 
   if (apply.includes('vue-recommended')) {
     checkTopLevelElementOrder(descriptor.source, filePath)
+    checkElementAttributeOrder(descriptor.template, filePath)
   }
 
   if (apply.includes('vue-caution')) {
diff --git a/src/rulesReport.ts b/src/rulesReport.ts
index 3de13b7..3772742 100644
--- a/src/rulesReport.ts
+++ b/src/rulesReport.ts
@@ -15,6 +15,7 @@ import { reportSelfClosingComponents } from './rules/vue-strong/selfClosingCompo
 import { reportDirectiveShorthands } from './rules/vue-strong/directiveShorthands'
 import { reportFullWordComponentName } from './rules/vue-strong/fullWordComponentName'
 import { reportTopLevelElementOrder } from './rules/vue-recommended/topLevelElementOrder'
+import { reportElementAttributeOrder } from './rules/vue-recommended/elementAttributeOrder'
 import { reportTooManyProps } from './rules/rrd/tooManyProps'
 import { reportFunctionSize } from './rules/rrd/functionSize'
 import { reportParameterCount } from './rules/rrd/parameterCount'
@@ -47,6 +48,7 @@ export const reportRules = () => {
 
   // vue-recommended rules
   errors += reportTopLevelElementOrder()
+  errors += reportElementAttributeOrder()
 
   // vue-caution rules
   errors += reportImplicitParentChildCommunication()


Test patch:
diff --git a/src/rules/vue-recommended/elementAttributeOrder.test.ts b/src/rules/vue-recommended/elementAttributeOrder.test.ts
new file mode 100644
index 0000000..a05d9d0
--- /dev/null
+++ b/src/rules/vue-recommended/elementAttributeOrder.test.ts
@@ -0,0 +1,56 @@
+import type { SFCTemplateBlock } from '@vue/compiler-sfc'
+import { describe, expect, it, vi } from 'vitest'
+import { BG_RESET, BG_WARN } from '../asceeCodes'
+import { checkElementAttributeOrder, reportElementAttributeOrder } from './elementAttributeOrder'
+
+const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
+
+describe('checkElementAttributeOrder', () => {
+  it('should not report files where elements attribute order is correct', () => {
+    const template = {
+      content: `
+                <template>
+                    <div v-if="isVisible" id="app" ref="myDiv" v-on:click="handleClick"></div>
+                </template>
+            `,
+    } as SFCTemplateBlock
+    const filename = 'element-attribute-order.vue'
+    checkElementAttributeOrder(template, filename)
+    expect(reportElementAttributeOrder()).toBe(0)
+    expect(mockConsoleLog).not.toHaveBeenCalled()
+  })
+
+  it('should report files where elements attribute order are incorrect', () => {
+    const template = {
+      content: `
+                <template>
+                    <input v-on:input="handleInput" v-model="inputValue">
+                </template>
+            `,
+    } as SFCTemplateBlock
+    const filename = 'element-attribute-order-incorrect.vue'
+    checkElementAttributeOrder(template, filename)
+    expect(reportElementAttributeOrder()).toBe(1)
+    expect(mockConsoleLog).toHaveBeenCalled()
+    expect(mockConsoleLog).toHaveBeenLastCalledWith(
+            `- ${filename} tag has attributes out of order ${BG_WARN}(input)${BG_RESET} 🚨`,
+    )
+  })
+
+  it('should not report files where elements attribute order is incorrect 2', () => {
+    const template = {
+      content: `
+                <template>
+                    <div id="app" v-if="isVisible" ref="myDiv" v-on:click="handleClick"></div>
+                </template>
+            `,
+    } as SFCTemplateBlock
+    const filename = 'element-attribute-order-incorrect-2.vue'
+    checkElementAttributeOrder(template, filename)
+    expect(reportElementAttributeOrder()).toBe(2)
+    expect(mockConsoleLog).toHaveBeenCalled()
+    expect(mockConsoleLog).toHaveBeenLastCalledWith(
+            `- ${filename} tag has attributes out of order ${BG_WARN}(div)${BG_RESET} 🚨`,
+    )
+  })
+})