Repository: rrd108/vue-mess-detector (ts)

Issue:
add rule (rrd) no direct dom access
 `getElementByXXX`
 `querySelectorXXX`


Pull request description:
feat: add new rule rrd no direct dom access with tests (#207)
### Summary
Add new rrd rule noDirectDomAccess with tests and docs page

### Description
- Added rule file
- Added test file
- Added docs page

### Related Issues
Fixes #207 

### Type of Change
- [ ] 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 b6b4722..7f06845 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -82,6 +82,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 Direct Dom Access', link: '/rules/rrd/no-direct-dom-access' },
                 { text: 'No Inline Styles', link: '/rules/rrd/no-inline-styles' },
                 { text: 'No Props Destructure', link: '/rules/rrd/no-prop-destructure' },
                 { text: 'No Ts Lang', link: '/rules/rrd/no-ts-lang' },
diff --git a/docs/rules/rrd/index.md b/docs/rules/rrd/index.md
index 25e94d0..629ed5c 100644
--- a/docs/rules/rrd/index.md
+++ b/docs/rules/rrd/index.md
@@ -16,6 +16,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 Direct Dom Access](./no-direct-dom-access.md)
 - [No Inline Styles](./no-inline-styles.md)
 - [No Prop Destructing](./no-prop-destructure.md)
 - [No Ts Lang](./no-ts-lang.md)
diff --git a/docs/rules/rrd/no-direct-dom-access.md b/docs/rules/rrd/no-direct-dom-access.md
new file mode 100644
index 0000000..3ec7c66
--- /dev/null
+++ b/docs/rules/rrd/no-direct-dom-access.md
@@ -0,0 +1,58 @@
+# No Direct Dom Access
+
+Checks if a component is directly accessing the DOM using methods like `getElementById`, `getElementsByClassName`, `getElementsByTagName`, `querySelector`, or `querySelectorAll`.
+
+## ❓ Why it's good to follow this rule?
+
+Direct DOM manipulation in Vue components can lead to several issues:
+
+1. It bypasses Vue's reactive system, potentially causing inconsistencies between the component's state and the DOM.
+2. It makes components less reusable and harder to test, as they become tightly coupled to specific DOM structures.
+3. It can lead to performance issues, as Vue's virtual DOM diffing algorithm is bypassed.
+4. It makes code harder to maintain and reason about, as the component's behavior is split between template/reactive data and imperative DOM manipulations.
+
+## 😱 Examples of code for which this rule will throw a warning
+
+::: warning
+The following code contains direct DOM access, which is discouraged in Vue components:
+:::
+
+```vue
+<script setup>
+function handleClick() {
+  const element = document.getElementById('myElement')
+  element.style.color = 'red'
+}
+</script>
+```
+
+## 🤩 How to fix it?
+
+::: tip
+Refactor your code to use Vue's reactive system and template bindings instead of direct DOM manipulation. Here's how to fix the previous example:
+:::
+
+```vue
+<script setup>
+import { ref } from 'vue'
+
+const elementColor = ref('black')
+
+function handleClick() {
+  elementColor.value = 'red'
+}
+</script>
+
+<template>
+  <div :style="{ color: elementColor }">My Element</div>
+</template>
+```
+
+In this fixed example:
+
+1. We use a template binding (`:style`) to control the element's color.
+2. We create a reactive `ref` called `elementColor` to store the color state.
+3. Instead of directly manipulating the DOM, we update the `elementColor` ref.
+4. Vue's reactivity system automatically updates the DOM when the `elementColor` value changes.
+
+This approach leverages Vue's strengths, making the code more maintainable, reusable, and performant. It also ensures that the component's state and the DOM remain in sync.
\ No newline at end of file
diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts
index 140fb6f..2e18cb4 100644
--- a/src/helpers/constants.ts
+++ b/src/helpers/constants.ts
@@ -10,6 +10,15 @@ export const OK_HEALTH_THRESHOLD = 95
 export const FLAT_RULES: string[] = [...Object.values(RULES).flat()]
 export const FLAT_RULESETS_RULES: string[] = [...RULESETS, ...FLAT_RULES]
 
+// Direct DOM access methods
+export const DOM_METHODS = [
+  'getElementById',
+  'getElementsByClassName',
+  'getElementsByTagName',
+  'querySelector',
+  'querySelectorAll',
+]
+
 // Override Config ~ Default
 export const DEFAULT_OVERRIDE_CONFIG: OverrideConfig = {
   maxExpressionLength: 40,
diff --git a/src/rules/rrd/index.ts b/src/rules/rrd/index.ts
index f522ea9..ce05f6d 100644
--- a/src/rules/rrd/index.ts
+++ b/src/rules/rrd/index.ts
@@ -12,6 +12,7 @@ export * from './hugeFiles'
 export * from './ifWithoutCurlyBraces'
 export * from './magicNumbers'
 export * from './nestedTernary'
+export * from './noDirectDomAccess'
 export * from './noInlineStyles'
 export * from './noPropDestructure'
 export * from './noTsLang'
diff --git a/src/rules/rrd/noDirectDomAccess.ts b/src/rules/rrd/noDirectDomAccess.ts
new file mode 100644
index 0000000..15c4d9b
--- /dev/null
+++ b/src/rules/rrd/noDirectDomAccess.ts
@@ -0,0 +1,53 @@
+import type { SFCScriptBlock } from '@vue/compiler-sfc'
+import type { FileCheckResult, Offense } from '../../types'
+import { anyOf, createRegExp, exactly, global, wordBoundary } from 'magic-regexp'
+import { DOM_METHODS } from '../../helpers/constants'
+import getLineNumber from '../getLineNumber'
+
+const results: FileCheckResult[] = []
+
+const resetResults = () => (results.length = 0)
+
+const checkNoDirectDomAccess = (script: SFCScriptBlock | null, filePath: string) => {
+  if (!script) {
+    return
+  }
+
+  const regex = createRegExp(
+    wordBoundary,
+    anyOf(...DOM_METHODS.map(method => exactly(method))),
+    '(',
+    [global],
+  )
+
+  const matches = script.content.match(regex)
+
+  matches?.forEach((match) => {
+    const lineNumber = getLineNumber(script.content.trim(), match)
+    results.push({
+      filePath,
+      message: `line #${lineNumber} <bg_warn>Direct DOM access detected: ${match}</bg_warn>`,
+    })
+  })
+}
+
+const reportNoDirectDomAccess = () => {
+  const offenses: Offense[] = []
+
+  if (results.length > 0) {
+    results.forEach((result) => {
+      offenses.push({
+        file: result.filePath,
+        rule: `<text_info>rrd ~ no direct dom access</text_info>`,
+        description: `👉 <text_warn>Avoid direct DOM manipulation in Vue components. Use refs or Vue's template syntax instead.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/no-direct-dom-access.html`,
+        message: `${result.message} 🚨`,
+      })
+    })
+  }
+
+  resetResults()
+
+  return offenses
+}
+
+export { checkNoDirectDomAccess, reportNoDirectDomAccess }
diff --git a/src/rules/rules.ts b/src/rules/rules.ts
index 2faa99b..520ace9 100644
--- a/src/rules/rules.ts
+++ b/src/rules/rules.ts
@@ -41,6 +41,7 @@ export const RULES = {
     'ifWithoutCurlyBraces',
     'magicNumbers',
     'nestedTernary',
+    'noDirectDomAccess',
     'noInlineStyles',
     'noPropDestructure',
     'noTsLang',
diff --git a/src/rulesCheck.ts b/src/rulesCheck.ts
index faddf49..67f9997 100644
--- a/src/rulesCheck.ts
+++ b/src/rulesCheck.ts
@@ -1,7 +1,7 @@
 import type { SFCDescriptor } from '@vue/compiler-sfc'
 import type { OverrideConfig } from './types/Override'
 import { getIsNuxt } from './context'
-import { checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkHugeFiles, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoInlineStyles, checkNoPropDestructure, checkNoTsLang, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd'
+import { checkBigVif, checkBigVshow, checkComplicatedConditions, checkComputedSideEffects, checkCyclomaticComplexity, checkDeepIndentation, checkElseCondition, checkFunctionSize, checkHtmlImageElements, checkHtmlLink, checkHugeFiles, checkIfWithoutCurlyBraces, checkMagicNumbers, checkNestedTernary, checkNoDirectDomAccess, checkNoInlineStyles, checkNoPropDestructure, checkNoTsLang, checkNoVarDeclaration, checkParameterCount, checkPlainScript, checkPropsDrilling, checkScriptLength, checkShortVariableName, checkTooManyProps, checkVForWithIndexKey, checkZeroLengthComparison } from './rules/rrd'
 import { RULES } from './rules/rules'
 import { checkApiWithoutMethod } from './rules/security'
 import { checkElementSelectorsWithScoped, checkImplicitParentChildCommunication } from './rules/vue-caution'
@@ -60,6 +60,8 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: s
     ifWithoutCurlyBraces: () => checkIfWithoutCurlyBraces(script, filePath),
     magicNumbers: () => checkMagicNumbers(script, filePath),
     nestedTernary: () => checkNestedTernary(script, filePath),
+    noDirectDomAccess: () => checkNoDirectDomAccess(script, filePath),
+    noInlineStyles: () => checkNoInlineStyles(descriptor.template, filePath),
     noPropDestructure: () => checkNoPropDestructure(script, filePath),
     noTsLang: () => isVueFile && checkNoTsLang(script, filePath),
     noVarDeclaration: () => checkNoVarDeclaration(script, filePath),
@@ -71,7 +73,6 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: s
     tooManyProps: () => checkTooManyProps(script, filePath, override.maxPropsCount),
     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 7569f7a..b6907b6 100644
--- a/src/rulesReport.ts
+++ b/src/rulesReport.ts
@@ -1,7 +1,7 @@
 import type { GroupBy, Health, Offense, OffensesGrouped, OutputLevel, ReportFunction, SortBy } from './types'
 import type { OverrideConfig } from './types/Override'
 import type { ReportOutput } from './types/ReportOutput'
-import { reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportHugeFiles, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoInlineStyles, reportNoPropDestructure, reportNoTsLang, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd'
+import { reportBigVif, reportBigVshow, reportComplicatedConditions, reportComputedSideEffects, reportCyclomaticComplexity, reportDeepIndentation, reportElseCondition, reportFunctionSize, reportHtmlImageElements, reportHtmlLink, reportHugeFiles, reportIfWithoutCurlyBraces, reportMagicNumbers, reportNestedTernary, reportNoDirectDomAccess, reportNoInlineStyles, reportNoPropDestructure, reportNoTsLang, reportNoVarDeclaration, reportParameterCount, reportPlainScript, reportPropsDrilling, reportScriptLength, reportShortVariableName, reportTooManyProps, reportVForWithIndexKey, reportZeroLengthComparison } from './rules/rrd'
 import { reportApiWithoutMethod } from './rules/security'
 import { reportElementSelectorsWithScoped, reportImplicitParentChildCommunication } from './rules/vue-caution'
 import { reportGlobalStyle, reportSimpleProp, reportSingleNameComponent, reportVforNoKey, reportVifWithVfor } from './rules/vue-essential'
@@ -74,6 +74,8 @@ export const reportRules = (groupBy: GroupBy, sortBy: SortBy, level: OutputLevel
   processOffenses(reportIfWithoutCurlyBraces)
   processOffenses(reportMagicNumbers)
   processOffenses(reportNestedTernary)
+  processOffenses(reportNoDirectDomAccess)
+  processOffenses(reportNoInlineStyles)
   processOffenses(reportNoPropDestructure)
   processOffenses(reportNoTsLang)
   processOffenses(reportNoVarDeclaration)
@@ -85,7 +87,6 @@ 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/noDirectDomAccess.test.ts b/src/rules/rrd/noDirectDomAccess.test.ts
new file mode 100644
index 0000000..c2b754b
--- /dev/null
+++ b/src/rules/rrd/noDirectDomAccess.test.ts
@@ -0,0 +1,113 @@
+import type { SFCScriptBlock } from '@vue/compiler-sfc'
+import { describe, expect, it } from 'vitest'
+import { checkNoDirectDomAccess, reportNoDirectDomAccess } from './noDirectDomAccess'
+
+describe('checkNoDirectDomAccess', () => {
+  it('should not report files without direct DOM access', () => {
+    const script = {
+      content: `
+        <script setup>
+        import { ref } from 'vue'
+        
+        const message = ref('Hello, Vue!')
+        const inputRef = ref(null)
+        
+        function focusInput() {
+          inputRef.value.focus()
+        }
+        </script>
+      `,
+    } as SFCScriptBlock
+    const fileName = 'noDirectDomAccess.vue'
+    checkNoDirectDomAccess(script, fileName)
+    const result = reportNoDirectDomAccess()
+    expect(result.length).toBe(0)
+    expect(result).toStrictEqual([])
+  })
+
+  it('should report files with direct DOM access using getElementById', () => {
+    const script = {
+      content: `
+        <script setup>
+        function handleClick() {
+          const element = document.getElementById('myElement')
+          element.style.color = 'red'
+        }
+        </script>
+      `,
+    } as SFCScriptBlock
+    const fileName = 'noDirectDomAccess-getElementById.vue'
+    checkNoDirectDomAccess(script, fileName)
+    const result = reportNoDirectDomAccess()
+    expect(result.length).toBe(1)
+    expect(result).toStrictEqual([{
+      file: fileName,
+      rule: `<text_info>rrd ~ no direct dom access</text_info>`,
+      description: `👉 <text_warn>Avoid direct DOM manipulation in Vue components. Use refs or Vue's template syntax instead.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/no-direct-dom-access.html`,
+      message: `line #3 <bg_warn>Direct DOM access detected: getElementById(</bg_warn> 🚨`,
+    }])
+  })
+
+  it('should report files with direct DOM access using querySelector', () => {
+    const script = {
+      content: `
+        <script setup>
+        import { onMounted } from 'vue'
+        
+        onMounted(() => {
+          const element = document.querySelector('.my-class')
+          element.textContent = 'Updated content'
+        })
+        </script>
+      `,
+    } as SFCScriptBlock
+    const fileName = 'noDirectDomAccess-querySelector.vue'
+    checkNoDirectDomAccess(script, fileName)
+    const result = reportNoDirectDomAccess()
+    expect(result.length).toBe(1)
+    expect(result).toStrictEqual([{
+      file: fileName,
+      rule: `<text_info>rrd ~ no direct dom access</text_info>`,
+      description: `👉 <text_warn>Avoid direct DOM manipulation in Vue components. Use refs or Vue's template syntax instead.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/no-direct-dom-access.html`,
+      message: `line #5 <bg_warn>Direct DOM access detected: querySelector(</bg_warn> 🚨`,
+    }])
+  })
+
+  it('should report multiple instances of direct DOM access', () => {
+    const script = {
+      content: `
+        <script setup>
+        function updateElements() {
+          const element1 = document.getElementById('element1')
+          const element2 = document.getElementsByClassName('my-class')
+          const element3 = document.querySelectorAll('.item')
+        }
+        </script>
+      `,
+    } as SFCScriptBlock
+    const fileName = 'noDirectDomAccess-multiple.vue'
+    checkNoDirectDomAccess(script, fileName)
+    const result = reportNoDirectDomAccess()
+    expect(result.length).toBe(3)
+    expect(result).toStrictEqual([
+      {
+        file: fileName,
+        rule: `<text_info>rrd ~ no direct dom access</text_info>`,
+        description: `👉 <text_warn>Avoid direct DOM manipulation in Vue components. Use refs or Vue's template syntax instead.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/no-direct-dom-access.html`,
+        message: `line #3 <bg_warn>Direct DOM access detected: getElementById(</bg_warn> 🚨`,
+      },
+      {
+        file: fileName,
+        rule: `<text_info>rrd ~ no direct dom access</text_info>`,
+        description: `👉 <text_warn>Avoid direct DOM manipulation in Vue components. Use refs or Vue's template syntax instead.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/no-direct-dom-access.html`,
+        message: `line #4 <bg_warn>Direct DOM access detected: getElementsByClassName(</bg_warn> 🚨`,
+      },
+      {
+        file: fileName,
+        rule: `<text_info>rrd ~ no direct dom access</text_info>`,
+        description: `👉 <text_warn>Avoid direct DOM manipulation in Vue components. Use refs or Vue's template syntax instead.</text_warn> See: https://vue-mess-detector.webmania.cc/rules/rrd/no-direct-dom-access.html`,
+        message: `line #5 <bg_warn>Direct DOM access detected: querySelectorAll(</bg_warn> 🚨`,
+      },
+    ])
+  })
+})