Repository: rrd108/vue-mess-detector
Pull request #224 (2024-09-04 21:42:21)

feat: add rule no inline styles
### Summary
A new rule was added and the respective documentation was also added.

### Related Issues
#206

### Type of Change
<!-- Please select the options that best describe your changes -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [x] Documentation update



Patch:
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index f66d0b3..31f01de 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -81,6 +81,7 @@ export default defineConfig({
                 { text: 'If Without Curly Braces', link: '/rules/rrd/if-without-curly-braces' },
                 { text: 'Magic Numbers', link: '/rules/rrd/magic-numbers' },
                 { text: 'Nested Ternary', link: '/rules/rrd/nested-ternary' },
+                { text: 'No Inline Styles', link: '/rules/rrd/no-inline-styles' },
                 { text: 'No Props Destructure', link: '/rules/rrd/no-prop-destructure' },
                 { text: 'No Var Declaration', link: '/rules/rrd/no-var-declaration' },
                 { text: 'Parameter Count', link: '/rules/rrd/parameter-count' },
diff --git a/docs/rules/rrd/index.md b/docs/rules/rrd/index.md
index 6bfa2ea..05af096 100644
--- a/docs/rules/rrd/index.md
+++ b/docs/rules/rrd/index.md
@@ -15,6 +15,7 @@ These ruleset is the most opinionated with rules that are not part of the _offic
 - [If Without Curly Braces](./if-without-curly-braces.md)
 - [Magic Numbers](./magic-numbers.md)
 - [Nested Ternary](./nested-ternary.md)
+- [No Inline Styles](./no-inline-styles.md)
 - [No Prop Destructing](./no-prop-destructure.md)
 - [No Var Declaration](./no-var-declaration.md)
 - [Parameter Count](./parameter-count.md)
diff --git a/docs/rules/rrd/no-inline-styles.md b/docs/rules/rrd/no-inline-styles.md
new file mode 100644
index 0000000..b6e6a47
--- /dev/null
+++ b/docs/rules/rrd/no-inline-styles.md
@@ -0,0 +1,43 @@
+# No Inline Styles
+
+Checks if in an element within the template of an SFC uses a inline style 
+
+## ❓ Why it's good to follow this rule?
+
+**Maintainability:** External CSS makes updates easier and keeps the codebase clean.
+**Performance:** Reduces HTML file size and leverages browser caching.
+**Consistency:** Ensures a uniform style across the entire website or application.
+
+## 😱 Examples of code for which this rule will throw a warning
+
+::: warning
+The following code contains inline `styles` which are not recommended because they increase the html code and do not take advantage of browser caching 
+:::
+
+```vue
+<template>
+  <div style="background-color: #ff0000;">
+    <!-- ... -->
+  </div>
+</template>
+```
+
+## 🤩 How to fix it?
+
+::: tip
+Using `style` with the `scoped` attribute and creating the necessary classes for the style you want to apply can be a good option.
+:::
+
+```vue
+<template>
+  <div class="bg-primary">
+    <!-- ... -->
+  </div>
+</template>
+
+<style scoped>
+.bg-primary {
+  background-color: #ff0000;
+}
+</style>
+```
diff --git a/src/rules/rrd/index.ts b/src/rules/rrd/index.ts
index 744c526..9e1ef29 100644
--- a/src/rules/rrd/index.ts
+++ b/src/rules/rrd/index.ts
@@ -21,3 +21,4 @@ export * from './shortVariableName'
 export * from './tooManyProps'
 export * from './vForWithIndexKey'
 export * from './zeroLengthComparison'
+export * from './noInlineStyles'
diff --git a/src/rules/rrd/noInlineStyles.ts b/src/rules/rrd/noInlineStyles.ts
new file mode 100644
index 0000000..38f33a9
--- /dev/null
+++ b/src/rules/rrd/noInlineStyles.ts
@@ -0,0 +1,45 @@
+import type { SFCTemplateBlock } from '@vue/compiler-sfc'
+import getLineNumber from '../getLineNumber'
+import type { FileCheckResult, Offense } from '../../types'
+import { BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'
+
+const results: FileCheckResult[] = []
+
+const checkNoInlineStyles = (template: SFCTemplateBlock | null, filePath: string) => {
+  if (!template) {
+    return
+  }
+
+  const regex = /style\s*=\s*['"][^'"]*['"]/g
+
+  const matches = [...template.content.matchAll(regex)]
+  let from = 0
+  matches?.forEach((match) => {
+    const lineNumber = getLineNumber(template.content.trim(), match[0], from)
+    results.push({
+      filePath,
+      message: `line #${lineNumber} ${BG_WARN}Found inline style: ${match[0]}${BG_RESET}`,
+    })
+    from = lineNumber
+  })
+}
+
+const reportNoInlineStyles = () => {
+  const offenses: Offense[] = []
+
+  if (results.length > 0) {
+    results.forEach((result) => {
+      offenses.push({
+        file: result.filePath,
+        rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`,
+        description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`,
+        message: `${result.message} 🚨`,
+      })
+    })
+  }
+  return offenses
+}
+
+const resetNoInlineStyles = () => (results.length = 0)
+
+export { checkNoInlineStyles, reportNoInlineStyles, resetNoInlineStyles }
diff --git a/src/rules/rules.ts b/src/rules/rules.ts
index 4003cf4..6b60533 100644
--- a/src/rules/rules.ts
+++ b/src/rules/rules.ts
@@ -40,6 +40,7 @@ export const RULES = {
     'ifWithoutCurlyBraces',
     'magicNumbers',
     'nestedTernary',
+    'noInlineStyles',
     'noPropDestructure',
     'noVarDeclaration',
     'parameterCount',
diff --git a/src/rulesCheck.ts b/src/rulesCheck.ts
index fce4660..6e3aa6d 100644
--- a/src/rulesCheck.ts
+++ b/src/rulesCheck.ts
@@ -5,7 +5,7 @@ import { checkElementSelectorsWithScoped, checkImplicitParentChildCommunication
 import { checkGlobalStyle, checkSimpleProp, checkSingleNameComponent, checkVforNoKey, checkVifWithVfor } from './rules/vue-essential'
 import { checkElementAttributeOrder, checkTopLevelElementOrder } from './rules/vue-recommended'
 import { checkComponentFilenameCasing, checkComponentFiles, checkDirectiveShorthands, checkFullWordComponentName, checkMultiAttributeElements, checkPropNameCasing, checkQuotedAttributeValues, checkSelfClosingComponents, checkSimpleComputed, checkTemplateSimpleExpression } from './rules/vue-strong'
-import { checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoPropDestructure, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd'
+import { checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoInlineStyles, checkNoPropDestructure, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd'
 import { getIsNuxt } from './context'
 
 export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: string[]) => {
@@ -67,6 +67,7 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: s
     tooManyProps: () => checkTooManyProps(script, filePath),
     vForWithIndexKey: () => isVueFile && checkVForWithIndexKey(descriptor.template, filePath),
     zeroLengthComparison: () => checkZeroLengthComparison(script, filePath),
+    noInlineStyles: () => checkNoInlineStyles(descriptor.template, filePath),
   }
 
   // Run the checks for each applied rule or ruleset
diff --git a/src/rulesReport.ts b/src/rulesReport.ts
index d213caf..baa1a80 100644
--- a/src/rulesReport.ts
+++ b/src/rulesReport.ts
@@ -5,7 +5,7 @@ import { reportElementSelectorsWithScoped, reportImplicitParentChildCommunicatio
 import { reportGlobalStyle, reportSimpleProp, reportSingleNameComponent, reportVforNoKey, reportVifWithVfor } from './rules/vue-essential'
 import { reportElementAttributeOrder, reportTopLevelElementOrder } from './rules/vue-recommended'
 import { reportComponentFilenameCasing, reportComponentFiles, reportDirectiveShorthands, reportFullWordComponentName, reportMultiAttributeElements, reportPropNameCasing, reportQuotedAttributeValues, reportSelfClosingComponents, reportSimpleComputed, reportTemplateSimpleExpression } from './rules/vue-strong'
-import { reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoPropDestructure, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd'
+import { reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoInlineStyles, reportNoPropDestructure, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd'
 
 // TODO move out to types
 interface OutputType {
@@ -85,6 +85,7 @@ export const reportRules = (groupBy: GroupBy, sortBy: SortBy, level: OutputLevel
   processOffenses(reportTooManyProps)
   processOffenses(reportVForWithIndexKey)
   processOffenses(reportZeroLengthComparison)
+  processOffenses(reportNoInlineStyles)
 
   // Sort offenses grouped by key based on the `sortBy` parameter
   const sortedKeys = Object.keys(offensesGrouped).sort((a, b) => {


Test patch:
diff --git a/src/rules/rrd/noInlineStyles.test.ts b/src/rules/rrd/noInlineStyles.test.ts
new file mode 100644
index 0000000..140e27b
--- /dev/null
+++ b/src/rules/rrd/noInlineStyles.test.ts
@@ -0,0 +1,57 @@
+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 { checkNoInlineStyles, reportNoInlineStyles, resetNoInlineStyles } from './noInlineStyles'
+
+describe('checkNoInlineStyles', () => {
+  beforeEach(() => {
+    resetNoInlineStyles()
+  })
+
+  it('should not report files with inline styles', () => {
+    const template = {
+      content: `
+        <template>
+          <div>Hello World!</div>
+          <div>
+            <span>Hi!</span>
+          </div>
+        </template>
+      `,
+    } as SFCTemplateBlock
+    const fileName = 'noInlineStyles.vue'
+    checkNoInlineStyles(template, fileName)
+    expect(reportNoInlineStyles().length).toBe(0)
+    expect(reportNoInlineStyles()).toStrictEqual([])
+  })
+
+  it('should report files with inline styles', () => {
+    const template = {
+      content: `
+        <template>
+          <div style="background: #fff;">Hello World!</div>
+          <div>
+            <span style="color: red;">Hi!</span>
+          </div>
+        </template>
+      `,
+    } as SFCTemplateBlock
+    const fileName = 'noInlineStyles-problem.vue'
+    checkNoInlineStyles(template, fileName)
+    expect(reportNoInlineStyles().length).toBe(2)
+    expect(reportNoInlineStyles()).toStrictEqual([
+      {
+        file: fileName,
+        rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`,
+        description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`,
+        message: `line #2 ${BG_WARN}Found inline style: style="background: #fff;"${BG_RESET} 🚨`,
+      },
+      {
+        file: fileName,
+        rule: `${TEXT_INFO}rrd ~ no Inline Styles${TEXT_RESET}`,
+        description: `👉 ${TEXT_WARN}Avoid using inline styles. Consider moving the styles to a CSS or SCSS file, or use a Vue scoped style.${TEXT_RESET}`,
+        message: `line #4 ${BG_WARN}Found inline style: style="color: red;"${BG_RESET} 🚨`,
+      },
+    ])
+  })
+})