Repository: rrd108/vue-mess-detector
Pull request #136 (2024-08-13 06:47:46)

feat: add rrd if without curly braces rule w tests


Patch:
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index ce95c73..a1a6429 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -70,6 +70,7 @@ export default defineConfig({
                 { text: 'Else Condition', link: '/rules/rrd/else-condition' },
                 { text: 'Function Size', link: '/rules/rrd/function-size' },
                 { text: 'HTML links', link: '/rules/rrd/html-link' },
+                { text: 'If Without Curly Braces', link: '/rules/rrd/if-without-curly-braces' },
                 { text: 'Magic Numbers', link: '/rules/rrd/magic-numbers' },
                 { text: 'Parameter Count', link: '/rules/rrd/parameter-count' },
                 { text: 'Plain Script', link: '/rules/rrd/plain-script' },
diff --git a/docs/rules/rrd/if-without-curly-braces.md b/docs/rules/rrd/if-without-curly-braces.md
new file mode 100644
index 0000000..433b06f
--- /dev/null
+++ b/docs/rules/rrd/if-without-curly-braces.md
@@ -0,0 +1,37 @@
+# If Without Curly Braces
+
+Check if `if` statements are missing curly braces, even when they contain only one line of code.
+
+## 📖 What does this rule enforce?
+
+This rule ensures that all `if` statements, regardless of the number of lines they contain, use curly braces `{}` to enclose the code block.
+
+## ❓ Why it's good to follow this rule?
+
+- **Readability:** Curly braces make the structure of the code clear, reducing the likelihood of misunderstandings. Even if the `if` statement has only one line, using braces ensures that the code is easy to read and follow.
+- **Consistency:** Enforcing curly braces for all `if` statements helps maintain a consistent coding style throughout the project, making the codebase easier to navigate.
+- **Maintainability:** Code changes often, and what starts as a one-line `if` statement can easily grow into something more complex. If braces are already in place, adding new lines of code becomes less error-prone.
+- **Avoiding Bugs:** Omitting braces can lead to subtle bugs, especially when additional statements are added later. With braces, there's no ambiguity about which statements are controlled by the `if`.
+
+## 😱 Examples of code for which this rule will throw a warning
+
+::: warning
+The following code omits curly braces in the `if` statement:
+:::
+
+```ts
+if (condition)
+  doSomething()
+```
+
+## 🤩 How to fix it?
+
+::: tip
+Always use curly braces to enclose the code block within `if` statements, even if it's just one line.
+:::
+
+```ts
+if (condition) {
+  doSomething()
+}
+```
diff --git a/docs/rules/rrd/index.md b/docs/rules/rrd/index.md
index e21c144..a8f44e9 100644
--- a/docs/rules/rrd/index.md
+++ b/docs/rules/rrd/index.md
@@ -7,6 +7,7 @@ These ruleset is the most opinionated with rules that are not part of the *offic
 - [Else Condition](./else-condition.md)
 - [Function Size](./function-size.md)
 - [HTML links](./html-link.md)
+- [If Without Curly Braces](./if-without-curly-braces.md)
 - [Magic Numbers](./magic-numbers.md)
 - [Parameter Count](./parameter-count.md)
 - [Plain Script](./plain-script.md)
diff --git a/src/rules/rrd/ifWithoutCurlyBraces.ts b/src/rules/rrd/ifWithoutCurlyBraces.ts
new file mode 100644
index 0000000..e66f248
--- /dev/null
+++ b/src/rules/rrd/ifWithoutCurlyBraces.ts
@@ -0,0 +1,50 @@
+import type { SFCScriptBlock } from '@vue/compiler-sfc'
+import { BG_ERR, BG_RESET, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'
+import type { FileCheckResult, Offense } from '../../types'
+
+const results: FileCheckResult[] = []
+
+const checkIfWithoutCurlyBraces = (script: SFCScriptBlock | null, filePath: string) => {
+  if (!script) {
+    return
+  }
+
+  const content = script.content
+  const lines = content.split('\n')
+
+  lines.forEach((line, index) => {
+    const trimmedLine = line.trim()
+
+    // Check if the line contains an `if` statement and is not followed by curly braces
+    if (trimmedLine.startsWith('if (') && !trimmedLine.includes('{')) {
+      // Check if the next line is not a continuation of the if statement with curly braces
+      const nextLine = lines[index + 1]?.trim()
+      if (!nextLine || (!nextLine.startsWith('{') && !trimmedLine.endsWith('{'))) {
+        results.push({
+          filePath,
+          message: `line #${index} if statement without curly braces: ${BG_ERR}${trimmedLine}${BG_RESET}`,
+        })
+      }
+    }
+  })
+}
+
+const reportIfWithoutCurlyBraces = () => {
+  const offenses: Offense[] = []
+
+  if (results.length > 0) {
+    results.forEach((result) => {
+      offenses.push({
+        file: result.filePath,
+        rule: `${TEXT_INFO}rrd ~ if without curly braces${TEXT_RESET}`,
+        description: `👉 ${TEXT_WARN}All if statements must be enclosed in curly braces for better readability and maintainability.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/if-without-curly-braces.html`,
+        message: `${result.message} 🚨`,
+      })
+    })
+  }
+  return offenses
+}
+
+const resetIfWithoutCurlyBraces = () => (results.length = 0)
+
+export { checkIfWithoutCurlyBraces, reportIfWithoutCurlyBraces, resetIfWithoutCurlyBraces }
diff --git a/src/rules/rules.ts b/src/rules/rules.ts
index 79ce381..16ab11b 100644
--- a/src/rules/rules.ts
+++ b/src/rules/rules.ts
@@ -20,6 +20,7 @@ export const RULES = {
     'elseCondition',
     'functionSize',
     'htmlLink',
+    'ifWithoutCurlyBraces',
     'magicNumbers',
     'parameterCount',
     'plainScript',
diff --git a/src/rulesCheck.ts b/src/rulesCheck.ts
index 235f12f..d2429f5 100644
--- a/src/rulesCheck.ts
+++ b/src/rulesCheck.ts
@@ -30,6 +30,7 @@ import { checkElementSelectorsWithScoped } from './rules/vue-caution/elementSele
 import { checkHtmlLink } from './rules/rrd/htmlLink'
 import { checkMagicNumbers } from './rules/rrd/magicNumbers'
 import { checkMultiAttributeElements } from './rules/vue-strong/multiAttributeElements'
+import { checkIfWithoutCurlyBraces } from './rules/rrd/ifWithoutCurlyBraces'
 
 export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: Array<RuleSetType>) => {
   const script = descriptor.scriptSetup || descriptor.script
@@ -72,6 +73,7 @@ export const checkRules = (descriptor: SFCDescriptor, filePath: string, apply: A
     checkElseCondition(script, filePath)
     checkFunctionSize(script, filePath)
     checkHtmlLink(descriptor.template, filePath)
+    checkIfWithoutCurlyBraces(script, filePath)
     checkMagicNumbers(script, filePath)
     checkParameterCount(script, filePath)
     checkPlainScript(descriptor.script, filePath)
diff --git a/src/rulesReport.ts b/src/rulesReport.ts
index 6bcabd7..98f464a 100644
--- a/src/rulesReport.ts
+++ b/src/rulesReport.ts
@@ -29,6 +29,7 @@ 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'
+import { reportIfWithoutCurlyBraces } from './rules/rrd/ifWithoutCurlyBraces'
 
 export const reportRules = (groupBy: GroupBy) => {
   let errors = 0
@@ -86,6 +87,7 @@ export const reportRules = (groupBy: GroupBy) => {
   processOffenses(reportElseCondition)
   processOffenses(reportFunctionSize)
   processOffenses(reportHtmlLink)
+  processOffenses(reportIfWithoutCurlyBraces)
   processOffenses(reportMagicNumbers)
   processOffenses(reportParameterCount)
   processOffenses(reportPlainScript)


Test patch:
diff --git a/src/rules/rrd/ifWithoutCurlyBraces.test.ts b/src/rules/rrd/ifWithoutCurlyBraces.test.ts
new file mode 100644
index 0000000..aa46237
--- /dev/null
+++ b/src/rules/rrd/ifWithoutCurlyBraces.test.ts
@@ -0,0 +1,86 @@
+import { beforeEach, describe, expect, it } from 'vitest'
+import type { SFCScriptBlock } from '@vue/compiler-sfc'
+import { BG_ERR, BG_RESET, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'
+import { checkIfWithoutCurlyBraces, reportIfWithoutCurlyBraces, resetIfWithoutCurlyBraces } from './ifWithoutCurlyBraces'
+
+describe('ifWithoutCurlyBraces', () => {
+  beforeEach(() => {
+    resetIfWithoutCurlyBraces()
+  })
+
+  it('should not report files where if statements use curly braces', () => {
+    const script = {
+      content: `
+        <script setup>
+        if (isLoading) {
+          doSomething();
+        }
+        </script>
+      `,
+    } as SFCScriptBlock
+    const filename = 'if-with-curly-braces.vue'
+    checkIfWithoutCurlyBraces(script, filename)
+    expect(reportIfWithoutCurlyBraces().length).toBe(0)
+    expect(reportIfWithoutCurlyBraces()).toStrictEqual([])
+  })
+
+  it('should not report files where if statements are written in a single line with curly braces', () => {
+    const script = {
+      content: `
+        <script setup>
+        if (isLoading) { doSomething(); }
+        if (isError) { handleError(); }
+        </script>
+      `,
+    } as SFCScriptBlock
+    const filename = 'if-with-single-line-curly-braces.vue'
+    checkIfWithoutCurlyBraces(script, filename)
+    expect(reportIfWithoutCurlyBraces().length).toBe(0)
+    expect(reportIfWithoutCurlyBraces()).toStrictEqual([])
+  })
+
+  it('should report files where an if statement does not use curly braces', () => {
+    const script = {
+      content: `
+        <script setup>
+        if (isLoading) doSomething();
+        </script>
+      `,
+    } as SFCScriptBlock
+    const filename = 'if-without-curly-braces.vue'
+    const statement = 'if (isLoading) doSomething();'
+    checkIfWithoutCurlyBraces(script, filename)
+    expect(reportIfWithoutCurlyBraces().length).toBe(1)
+    expect(reportIfWithoutCurlyBraces()).toStrictEqual([{
+      file: filename,
+      rule: `${TEXT_INFO}rrd ~ if without curly braces${TEXT_RESET}`,
+      description: `👉 ${TEXT_WARN}All if statements must be enclosed in curly braces for better readability and maintainability.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/if-without-curly-braces.html`,
+      message: `line #2 if statement without curly braces: ${BG_ERR}${statement}${BG_RESET} 🚨`,
+    }])
+  })
+
+  it('should report files where multiple if statements do not use curly braces', () => {
+    const script = {
+      content: `
+        <script setup>
+        if (isLoading) doSomething();
+        if (isError) handleError();
+        </script>
+      `,
+    } as SFCScriptBlock
+    const filename = 'multiple-if-without-curly-braces.vue'
+    checkIfWithoutCurlyBraces(script, filename)
+    expect(reportIfWithoutCurlyBraces().length).toBe(2)
+    expect(reportIfWithoutCurlyBraces()).toStrictEqual([{
+      file: filename,
+      rule: `${TEXT_INFO}rrd ~ if without curly braces${TEXT_RESET}`,
+      description: `👉 ${TEXT_WARN}All if statements must be enclosed in curly braces for better readability and maintainability.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/if-without-curly-braces.html`,
+      message: `line #2 if statement without curly braces: ${BG_ERR}if (isLoading) doSomething();${BG_RESET} 🚨`,
+    }, {
+      file: filename,
+      rule: `${TEXT_INFO}rrd ~ if without curly braces${TEXT_RESET}`,
+      description: `👉 ${TEXT_WARN}All if statements must be enclosed in curly braces for better readability and maintainability.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/if-without-curly-braces.html`,
+      message: `line #3 if statement without curly braces: ${BG_ERR}if (isError) handleError();${BG_RESET} 🚨`,
+    }])
+  })
+})