Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add vue-strong multi attribute element rule plus fixes #128

Merged
merged 4 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default defineConfig({
{ text: 'Self Closing Components', link: '/rules/vue-strong/self-closing-components' },
{ text: 'Simple Computed Properties', link: '/rules/vue-strong/simple-computed' },
{ text: 'Template Simple Expressions', link: '/rules/vue-strong/template-simple-expression' },
{ text: 'Multi Attribute Elements', link: '/rules/vue-strong/multi-attribute-elements' },
] },
{ text: 'Vue Recommended', link: '/rules/vue-recommended', collapsed: true, items: [
{ text: 'Element Attribute Order', link: '/rules/vue-recommended/element-attribute-order' },
Expand Down
1 change: 1 addition & 0 deletions docs/rules/vue-strong/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ These rules have been found to improve readability and/or developer experience i
- [Self Closing Components](./self-closing-components.md)
- [Simple Computed Properties](./simple-computed.md)
- [Template Simple Expressions](./template-simple-expression.md)
- [Multi Attribute Elements](./multi-attribute-elements.md)
11 changes: 11 additions & 0 deletions docs/rules/vue-strong/multi-attribute-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Multi-Attribute Elements

Checks that elements with multiple attributes span multiple lines, with one attribute per line.
👉 https://vuejs.org/style-guide/rules-strongly-recommended.html#multi-attribute-elements

## ❓ Why it's good to follow this rule?

- **Readability**: Spreading attributes across multiple lines makes it easier to read and understand the code.
- **Maintainability**: When attributes are on separate lines, it's simpler to add, remove, or modify them without affecting other attributes.
- **Consistency**: Following this convention helps maintain a consistent style across components, making it easier for teams to collaborate.
- **Debugging**: Having one attribute per line makes it easier to identify issues related to specific attributes, especially when debugging.
51 changes: 51 additions & 0 deletions src/rules/rrd/plainScript.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it } from 'vitest'
import type { SFCScriptBlock } from '@vue/compiler-sfc'
import { BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'
import { checkPlainScript, reportPlainScript, resetPlainScript } from './plainScript'

describe('plainScript', () => {
beforeEach(() => {
resetPlainScript()
})

it('should not report files with <script setup>', () => {
const script = {
content: `
<script setup>
const message = 'Hello, world!';
</script>
`,
setup: true,
} as SFCScriptBlock
const filename = 'with-script-setup.vue'
checkPlainScript(script, filename)
expect(reportPlainScript().length).toBe(0)
expect(reportPlainScript()).toStrictEqual([])
})

it('should report files with a plain <script> block', () => {
const script = {
content: `
<script>
export default {
data() {
return {
message: 'Hello, world!'
}
}
}
</script>
`,
setup: false,
} as SFCScriptBlock
const filename = 'plain-script.vue'
checkPlainScript(script, filename)
expect(reportPlainScript().length).toBe(1)
expect(reportPlainScript()).toStrictEqual([{
file: filename,
rule: `${TEXT_INFO}rrd ~ Plain <script> blocks${TEXT_RESET}`,
description: `👉 ${TEXT_WARN} Consider using <script setup> to leverage the new SFC <script> syntax.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/plain-script.html`,
message: `${BG_WARN}Plain <script> block${BG_RESET} found 🚨`,
}])
})
})
6 changes: 4 additions & 2 deletions src/rules/rrd/plainScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { FileCheckResult, Offense } from '../../types'
const results: FileCheckResult[] = []

const checkPlainScript = (script: SFCScriptBlock | null, filePath: string) => {
if (!script || !script.setup) {
if (!script || script.setup) {
return
}
results.push({ filePath, message: `${BG_WARN}Plain <script> block${BG_RESET} found` })
Expand All @@ -27,4 +27,6 @@ const reportPlainScript = () => {
return offenses
}

export { checkPlainScript, reportPlainScript }
const resetPlainScript = () => (results.length = 0)

export { checkPlainScript, reportPlainScript, resetPlainScript }
3 changes: 2 additions & 1 deletion src/rules/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ export const RULES = {
'componentFilenameCasing',
'componentFiles',
'directiveShorthands',
'fullWordComponentName',
'multiAttributeElements',
'propNameCasing',
'quotedAttributeValues',
'selfClosingComponents',
'simpleComputed',
'templateSimpleExpression',
'fullWordComponentName',
],
'rrd': [
'cyclomaticComplexity',
Expand Down
70 changes: 70 additions & 0 deletions src/rules/vue-strong/multiAttributeElements.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { beforeEach, describe, expect, it } from 'vitest'
import type { SFCTemplateBlock } from '@vue/compiler-sfc'
import { BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'
import { checkMultiAttributeElements, reportMultiAttributeElements, resetMultiAttributeElements } from './multiAttributeElements'

describe('multiAttributeElements', () => {
beforeEach(() => {
resetMultiAttributeElements()
})

it('should not report files where elements have single attributes', () => {
const template = {
content: `
<template>
<div id="app"></div>
<button @click="handleClick">Click me</button>
</template>
`,
} as SFCTemplateBlock
const filename = 'single-attribute-element.vue'
checkMultiAttributeElements(template, filename)
expect(reportMultiAttributeElements().length).toBe(0)
expect(reportMultiAttributeElements()).toStrictEqual([])
})

it('should report files where one element has multiple attributes on the same line', () => {
const template = {
content: `
<template>
<div id="app" class="container" style="color: red;"></div>
</template>
`,
} as SFCTemplateBlock
const filename = 'multi-attribute-element.vue'
const element = 'div'
checkMultiAttributeElements(template, filename)
expect(reportMultiAttributeElements().length).toBe(1)
expect(reportMultiAttributeElements()).toStrictEqual([{
file: filename,
rule: `${TEXT_INFO}vue-strong ~ multi-attribute elements${TEXT_RESET}`,
description: `👉 ${TEXT_WARN}Elements with multiple attributes should span multiple lines, with one attribute per line.${TEXT_RESET}`,
message: `Element ${BG_WARN}<${element}>${BG_RESET} should have its attributes on separate lines 🚨`,
}])
})

it('should report files where multiple elements have multiple attributes on the same line', () => {
const template = {
content: `
<template>
<div id="app" class="container" style="color: red;"></div>
<button type="button" class="btn" @click="handleClick">Click me</button>
</template>
`,
} as SFCTemplateBlock
const filename = 'multiple-multi-attribute-elements.vue'
checkMultiAttributeElements(template, filename)
expect(reportMultiAttributeElements().length).toBe(2)
expect(reportMultiAttributeElements()).toStrictEqual([{
file: filename,
rule: `${TEXT_INFO}vue-strong ~ multi-attribute elements${TEXT_RESET}`,
description: `👉 ${TEXT_WARN}Elements with multiple attributes should span multiple lines, with one attribute per line.${TEXT_RESET}`,
message: `Element ${BG_WARN}<div>${BG_RESET} should have its attributes on separate lines 🚨`,
}, {
file: filename,
rule: `${TEXT_INFO}vue-strong ~ multi-attribute elements${TEXT_RESET}`,
description: `👉 ${TEXT_WARN}Elements with multiple attributes should span multiple lines, with one attribute per line.${TEXT_RESET}`,
message: `Element ${BG_WARN}<button>${BG_RESET} should have its attributes on separate lines 🚨`,
}])
})
})
53 changes: 53 additions & 0 deletions src/rules/vue-strong/multiAttributeElements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { SFCTemplateBlock } from '@vue/compiler-sfc'
import type { FileCheckResult, Offense } from '../../types'
import { BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'

const results: FileCheckResult[] = []

const checkMultiAttributeElements = (template: SFCTemplateBlock | null, filePath: string) => {
if (!template)
return

// Regex to match elements with attributes
// eslint-disable-next-line regexp/no-super-linear-backtracking
const elementRegex = /<(\w+)([^>]*)>/g
let match: RegExpExecArray | null

// eslint-disable-next-line no-cond-assign
while ((match = elementRegex.exec(template.content)) !== null) {
const elementTag = match[1]
const attributesString = match[2]

// Check if there are multiple attributes
const attributes = attributesString.split(/\s+/).filter(attr => attr.trim() !== '')

if (attributes.length > 1) {
// Check if attributes are on separate lines
const attributeLines = attributesString.split('\n').length
if (attributeLines === 1) {
results.push({ filePath, message: `Element ${BG_WARN}<${elementTag}>${BG_RESET} should have its attributes on separate lines` })
}
}
}
}

const reportMultiAttributeElements = () => {
const offenses: Offense[] = []

if (results.length > 0) {
results.forEach((result) => {
offenses.push({
file: result.filePath,
rule: `${TEXT_INFO}vue-strong ~ multi-attribute elements${TEXT_RESET}`,
description: `👉 ${TEXT_WARN}Elements with multiple attributes should span multiple lines, with one attribute per line.${TEXT_RESET}`,
message: `${result.message} 🚨`,
})
})
}

return offenses
}

const resetMultiAttributeElements = () => (results.length = 0)

export { checkMultiAttributeElements, reportMultiAttributeElements, resetMultiAttributeElements }
2 changes: 2 additions & 0 deletions src/rulesCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { checkDeepIndentation } from './rules/rrd/deepIndentation'
import { checkElementSelectorsWithScoped } from './rules/vue-caution/elementSelectorsWithScoped'
import { checkHtmlLink } from './rules/rrd/htmlLink'
import { checkMagicNumbers } from './rules/rrd/magicNumbers'
import { checkMultiAttributeElements } from './rules/vue-strong/multiAttributeElements'

export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: Array<RuleSetType>) => {
const script = descriptor.scriptSetup || descriptor.script
Expand All @@ -52,6 +53,7 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: A
checkQuotedAttributeValues(descriptor, filePath)
checkDirectiveShorthands(descriptor, filePath)
checkFullWordComponentName(filePath)
checkMultiAttributeElements(descriptor.template, filePath)
}

if (apply.includes('vue-recommended')) {
Expand Down
14 changes: 9 additions & 5 deletions src/rulesReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { reportDeepIndentation } from './rules/rrd/deepIndentation'
import type { GroupBy, Offense, OffensesGrouped, ReportFunction } from './types'
import { reportHtmlLink } from './rules/rrd/htmlLink'
import { reportMagicNumbers } from './rules/rrd/magicNumbers'
import { reportMultiAttributeElements } from './rules/vue-strong/multiAttributeElements'
import { reportElementSelectorsWithScoped } from './rules/vue-caution/elementSelectorsWithScoped'

export const reportRules = (groupBy: GroupBy) => {
let errors = 0
Expand Down Expand Up @@ -60,21 +62,23 @@ export const reportRules = (groupBy: GroupBy) => {

// vue-strong rules
processOffenses(reportComponentFilenameCasing)
processOffenses(reportSelfClosingComponents)
processOffenses(reportComponentFiles)
processOffenses(reportDirectiveShorthands)
processOffenses(reportFullWordComponentName)
processOffenses(reportMultiAttributeElements)
processOffenses(reportPropNameCasing)
processOffenses(reportTemplateSimpleExpression)
processOffenses(reportQuotedAttributeValues)
processOffenses(reportDirectiveShorthands)
processOffenses(reportSelfClosingComponents)
processOffenses(reportSimpleComputed)
processOffenses(reportComponentFiles)
processOffenses(reportFullWordComponentName)
processOffenses(reportTemplateSimpleExpression)

// vue-recommended rules
processOffenses(reportTopLevelElementOrder)
processOffenses(reportElementAttributeOrder)

// vue-caution rules
processOffenses(reportImplicitParentChildCommunication)
processOffenses(reportElementSelectorsWithScoped)

// rrd rules
processOffenses(reportCyclomaticComplexity)
Expand Down
Loading