Repository: rrd108/vue-mess-detector
Pull request #59 (2024-07-15 22:01:33)

feat: add rrd function-size rule


Patch:
diff --git a/README.md b/README.md
index 37e9350..ab085f9 100644
--- a/README.md
+++ b/README.md
@@ -145,3 +145,7 @@ Checks if there are any `else` condition in the `<script>` block. This is a code
 ### Too many props (rrd)
 
 Checks if the component got more then 5 props.
+
+### Function Size (rrd)
+
+Checks if functions inside `<script setup>` block are less than 20 lines of code. It handles regular and arrow functions.
diff --git a/src/analyzer.ts b/src/analyzer.ts
index 9446a2f..df41bfd 100644
--- a/src/analyzer.ts
+++ b/src/analyzer.ts
@@ -18,6 +18,7 @@ import { checkQuotedAttributeValues, reportQuotedAttributeValues } from './rules
 import { checkSelfClosingComponents, reportSelfClosingComponents } from './rules/vue-strong/selfClosingComponents'
 import { checkDirectiveShorthands, reportDirectiveShorthands } from './rules/vue-strong/directiveShorthands'
 import { checkTooManyProps, reportTooManyProps } from './rules/rrd/tooManyProps'
+import { checkFunctionSize, reportFunctionSize } from './rules/rrd/functionSize'
 
 let filesCount = 0
 
@@ -82,6 +83,7 @@ export const analyze = (dir: string) => {
       checkCyclomaticComplexity(script, filePath)
       checkElseCondition(script, filePath)
       checkTooManyProps(script, filePath)
+      checkFunctionSize(script, filePath)
     }
 
     descriptor.styles.forEach(style => {
@@ -125,6 +127,7 @@ export const analyze = (dir: string) => {
   errors += reportCyclomaticComplexity()
   errors += reportElseCondition()
   errors += reportTooManyProps()
+  errors += reportFunctionSize()
 
   if (!errors) {
     console.log(`${BG_OK}No code smells detected!${BG_RESET}`)
diff --git a/src/rules/rrd/functionSize.ts b/src/rules/rrd/functionSize.ts
new file mode 100644
index 0000000..2df2459
--- /dev/null
+++ b/src/rules/rrd/functionSize.ts
@@ -0,0 +1,42 @@
+import { SFCScriptBlock } from '@vue/compiler-sfc';
+import { BG_RESET, BG_WARN, TEXT_WARN, TEXT_RESET, TEXT_INFO } from '../asceeCodes'
+
+const functionSizeFiles: { filename: string, funcName: string }[] = [];
+
+const MAX_FUNCTION_LENGTH = 20 // completely rrd made-up number
+
+const checkFunctionSize = (script: SFCScriptBlock, filePath: string) => {
+  // Regular expression to match function definitions (both regular and arrow functions)
+  const regex = /function\s+([a-zA-Z0-9_$]+)\s*\([^)]*\)\s*{([^{}]*(([^{}]*{[^{}]*}[^{}]*)*[^{}]*))}|const\s+([a-zA-Z0-9_$]+)\s*=\s*\([^)]*\)\s*=>\s*{([^{}]*(([^{}]*{[^{}]*}[^{}]*)*[^{}]*))}/g;
+  let match;
+
+  while ((match = regex.exec(script.content)) !== null) {
+    /*
+      We use match[1] and match[2] for regular functions
+      and match[5] and match[6] for arrow functions
+    */
+    const funcName = match[1] || match[5]; 
+    const funcBody = match[2] || match[6];
+    
+    // Check if the function block has more than `MAX_FUNCTION_LENGTH` lines
+    const lineCount = funcBody.split('\n').length;
+    if (lineCount > MAX_FUNCTION_LENGTH) {
+      functionSizeFiles.push({ filename: filePath, funcName });
+    }
+  }
+}
+
+const reportFunctionSize = () => {
+  if (functionSizeFiles.length > 0) {
+    console.log(
+      `\n${TEXT_INFO}rrd${TEXT_RESET} ${BG_WARN}function size${BG_RESET} exceeds recommended limit in ${functionSizeFiles.length} files.`
+    )
+    console.log(`👉 ${TEXT_WARN}Functions must be shorter than ${MAX_FUNCTION_LENGTH} lines${TEXT_RESET}`)
+    functionSizeFiles.forEach(file => {
+      console.log(`- ${file.filename} 🚨 ${BG_WARN}(${file.funcName})${BG_RESET}`)
+    })
+  }
+  return functionSizeFiles.length
+}
+
+export { checkFunctionSize, reportFunctionSize };


Test patch:
diff --git a/src/rules/rrd/functionSize.test.ts b/src/rules/rrd/functionSize.test.ts
new file mode 100644
index 0000000..88fa0ae
--- /dev/null
+++ b/src/rules/rrd/functionSize.test.ts
@@ -0,0 +1,146 @@
+import { describe, expect, it, vi } from 'vitest'
+import { SFCScriptBlock } from '@vue/compiler-sfc'
+import { checkFunctionSize, reportFunctionSize } from './functionSize'
+import { BG_RESET, BG_WARN } from '../asceeCodes'
+
+const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
+
+describe('checkFunctionSize', () => {
+  it('should not report files where functions do not exceed the recommended limit', () => {
+    const script = { 
+      content: `
+        <script setup>
+          function greeting(msg) {
+            if (!msg) {
+              return
+            }
+
+            return {
+              message: msg.trim() 
+            }
+          }
+        </script>
+      `
+    } as SFCScriptBlock;
+    const fileName = 'function-size.vue';
+    checkFunctionSize(script, fileName);
+    expect(reportFunctionSize()).toBe(0);
+    expect(mockConsoleLog).not.toHaveBeenCalled();
+  })
+
+  it('should report files with one function exceeding the limit', () => {
+    const script = { 
+      content: `
+        <script setup>
+          function dummyRegularFunction() {
+            const firstName = 'john';
+            const lastName = 'doe';
+            let age = 30;
+
+            if (age < 18) {
+              console.log('Too young for this function!')
+            } else {
+              console.log('Hello ', firstName)
+            }
+
+            //Some more lines of code
+            const hobbies = ['reading', 'gaming', 'cooking'];
+            for (const hobby of hobbies) {
+              console.log('I enjoy ', hobby);
+            }
+
+            // Even more lines...
+            const getRandomNumber = () => Math.floor(Math.random() * 100);
+            const randomNum = getRandomNumber();
+            console.log('Random number: ', randomNum);
+
+            // And a final line
+            return 'Function execution complete!';
+          }
+
+          const sayHello = () => {
+            console.log(getGreeting());
+          }
+        </script>
+      `
+    } as SFCScriptBlock;
+    const fileName = 'function-size.vue';
+    const funcName = 'dummyRegularFunction';
+    checkFunctionSize(script, fileName);
+    expect(reportFunctionSize()).toBe(1);
+    expect(mockConsoleLog).toHaveBeenCalled();
+    expect(mockConsoleLog).toHaveBeenLastCalledWith(`- ${fileName} 🚨 ${BG_WARN}(${funcName})${BG_RESET}`)
+  })
+
+  it('should report files with two functions exceeding the limit', () => {
+    const script = { 
+      content: `
+        <script setup>
+          function dummyRegularFunction(name = 'Hi') {
+            if (!name) {
+              return
+            }
+
+            let message = '';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+            message = 'Just testing this feature';
+
+            return message;
+          }
+
+          const dummyArrowFunction = (name) => {
+            const firstName = 'john';
+            const lastName = 'doe';
+            let age = 30;
+
+            if (age < 18) {
+              console.log('Too young for this function!')
+            } else {
+              console.log('Hello ', firstName)
+            }
+
+            // Some more lines of code
+            const hobbies = ['reading', 'gaming', 'cooking'];
+            for (const hobby of hobbies) {
+              console.log('I enjoy ', hobby);
+            }
+
+            // Even more lines...
+            const getRandomNumber = () => Math.floor(Math.random() * 100);
+            const randomNum = getRandomNumber();
+            console.log('Random number: ', randomNum);
+
+            // And a final line
+            return 'Function execution complete!';
+          }
+        </script>
+      `
+    } as SFCScriptBlock;
+    const fileName = 'function-size.vue';
+    const funcName = 'dummyArrowFunction';
+    checkFunctionSize(script, fileName);
+    expect(reportFunctionSize()).toBe(3);
+    expect(mockConsoleLog).toHaveBeenCalled();
+    expect(mockConsoleLog).toHaveBeenLastCalledWith(`- ${fileName} 🚨 ${BG_WARN}(${funcName})${BG_RESET}`)
+  })
+})
\ No newline at end of file