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

Issue:
function size rule output wrong function name and length and does not show line number for the function
**Describe the bug**
When I run the analyzer on vue-mess-detector the output shows questions as the function name instead of createFiles, and shows no line number.

**To Reproduce**
run `yarn analyze src/generator/createRule.js -l=error`

1. What is the exact code you are analyzing?
`src/generator/createRule.js`

3. What is the output you are getting?
- rrd ~ function size
   File: src/generator/createRule.js
   Description: 👉 Functions must be shorter than 20 lines. See: https://vue-mess-detector.webmania.cc/rules/rrd/function-size.html
   Message: function (questions) is too long 🚨

**Expected behavior**
Message should be function (createFiles) in line 19 is too long 

**Used version number of vue-mess-detector:**
0.38.0


Pull request description:
fix: huge refactor to the `functionSize.ts` check and parse methods plus more tests
### Summary
Refactor the checker and parse methods of the `functionSize` rule to fix bugs and false positives

### Description
- Refactor `checkFunctionSize` method
- Refactor utils/helpers inside `functionSize.ts` file
- Removed unused utils functions
- Add more tests
- Add lineNumber to the output

### Related Issues
Fixes #174 
Supports #153 

### Type of Change
- [X] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update

### Screenshots (if applicable)
![image](https://github.com/user-attachments/assets/5d945cef-f3e7-4bea-87ea-56771585c217)
![image](https://github.com/user-attachments/assets/335e95bf-6c69-42b5-ac3d-fd1ec71a3d6c)

And this helped improve the code health of the project itself
![image](https://github.com/user-attachments/assets/a0b83d86-7b87-4538-9a3f-1e896961132c)



Patch:
diff --git a/src/rules/rrd/functionSize.ts b/src/rules/rrd/functionSize.ts
index bb92200..876b0e6 100644
--- a/src/rules/rrd/functionSize.ts
+++ b/src/rules/rrd/functionSize.ts
@@ -1,110 +1,131 @@
 import type { SFCScriptBlock } from '@vue/compiler-sfc'
 import { BG_ERR, BG_RESET, BG_WARN, TEXT_INFO, TEXT_RESET, TEXT_WARN } from '../asceeCodes'
 import type { FileCheckResult, Offense } from '../../types'
+import getLineNumber from '../getLineNumber'
+
+interface AddFunctionToFilesParams {
+  funcName: string
+  funcBody: string
+  lineNumber: number
+  filePath: string
+}
 
 const results: FileCheckResult[] = []
 
-export const MAX_FUNCTION_LENGTH = 20 // completely rrd made-up number
+// Maximum allowed function length in lines
+// This is an arbitrary number defined by the rrd ruleset
+export const MAX_FUNCTION_LENGTH = 20
 
 const CONST_KEYWORD_LENGTH = 'const'.length
 const FUNCTION_KEYWORD_LENGTH = 'function'.length
 
-function addFunctionToFiles(funcName: string, funcBody: string, filePath: string) {
+function addFunctionToFiles({ funcName, funcBody, lineNumber, filePath }: AddFunctionToFilesParams) {
   const lineCount = funcBody.split('\n').length
+
+  const cleanedFuncName = cleanFunctionName(funcName)
+
   if (lineCount > 2 * MAX_FUNCTION_LENGTH) {
-    results.push({ filePath, message: `function ${BG_ERR}(${cleanFunctionName(funcName)})${BG_RESET} is too long: ${BG_ERR}${lineCount} lines${BG_RESET}` })
+    results.push({ filePath, message: `function ${BG_ERR}(${cleanedFuncName}#${lineNumber})${BG_RESET} is too long: ${BG_ERR}${lineCount} lines${BG_RESET}` })
     return
   }
-  if (lineCount > MAX_FUNCTION_LENGTH) {
-    results.push({ filePath, message: `function ${BG_WARN}(${cleanFunctionName(funcName)})${BG_RESET} is too long: ${BG_WARN}${lineCount} lines${BG_RESET}` })
+  if (lineCount >= MAX_FUNCTION_LENGTH) {
+    results.push({ filePath, message: `function ${BG_WARN}(${cleanedFuncName}#${lineNumber})${BG_RESET} is too long: ${BG_WARN}${lineCount} lines${BG_RESET}` })
   }
 }
 
-// Helper to parse the function name after the 'function' keyword
-function parseFunctionName(content: string, startIndex: number): string {
-  let name = ''
-  let i = startIndex
+function parseRegularFunction(content: string, startIndex: number) {
+  // eslint-disable-next-line regexp/prefer-w
+  const functionRegex = /function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*\([^)]*\)\s*\{/g
+  functionRegex.lastIndex = startIndex // Start search from the given index
 
-  // Skip whitespace and move to the function name
-  while (i < content.length && /\s/.test(content[i])) {
-    i++
-  }
+  const match = functionRegex.exec(content)
 
-  // Capture the function name and skip the 'const' keyword if it's an arrow function
-  if (content.slice(i, i + CONST_KEYWORD_LENGTH) === 'const') {
-    i += CONST_KEYWORD_LENGTH
-    // Skip any whitespace after 'const'
-    while (i < content.length && /\s/.test(content[i])) {
-      i++
-    }
-  }
+  if (match) {
+    const name = match[1]
+    const bodyStartIndex = functionRegex.lastIndex
+    let braceCount = 1 // To track the opening brace we just found
+    let currentIndex = bodyStartIndex
 
-  // Now capture the actual function name
-  while (i < content.length && /[\w$]/.test(content[i])) {
-    name += content[i]
-    i++
-  }
-
-  return name.trim()
-}
-
-// Helper to skip to the start of the function body
-function skipToFunctionBody(content: string, startIndex: number): number {
-  let i = startIndex
-
-  // Skip to the start of the function body '{'
-  while (i < content.length && content[i] !== '{') {
-    i++
-  }
-
-  return i + 1 // Move to the first character inside the '{'
-}
+    while (braceCount > 0 && currentIndex < content.length) {
+      if (content[currentIndex] === '{') {
+        braceCount++
+      }
+      else if (content[currentIndex] === '}') {
+        braceCount--
+      }
+      currentIndex++
+    }
 
-// Helper to parse arrow function
-function parseArrowFunction(content: string, startIndex: number) {
-  let name = ''
-  let bodyStart = -1
+    const body = content.slice(bodyStartIndex, currentIndex - 1).trim() // Extract the function body
 
-  // Find variable name
-  while (startIndex < content.length && content[startIndex] !== '=') {
-    if (/\w/.test(content[startIndex])) {
-      name += content[startIndex]
+    return {
+      name,
+      body,
+      end: currentIndex, // Returns the position after the matched function
     }
-    startIndex++
   }
-
-  // Skip arrow syntax (=>)
-  startIndex = content.indexOf('=>', startIndex)
-  if (startIndex === -1) {
+  else {
     return null
   }
-  bodyStart = startIndex + 2
-
-  return { name, bodyStart }
 }
 
-// Helper to extract the function body by counting braces
-function extractFunctionBody(content: string, startIndex: number) {
-  let openBraces = 1
-  let body = ''
-  let endIndex = startIndex
+function parseArrowFunction(content: string, index: number) {
+  // Define regex to match arrow functions with or without curly braces
+  // eslint-disable-next-line regexp/prefer-w, regexp/no-unused-capturing-group
+  const regex = /const\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*=\s*(async\s+)?\(([^)]*)\)\s*=>\s*/
+
+  // Extract the substring starting from the current index
+  const substring = content.slice(index)
+  const match = regex.exec(substring)
+
+  if (match) {
+    const [, name] = match
+    const bodyStartIndex = index + match.index + match[0].length // Calculate the start index of the function body
+    let bodyEndIndex = bodyStartIndex
+    let body = ''
+
+    if (content[bodyStartIndex] === '{') {
+      // If the function body starts with '{', we are dealing with a block arrow function
+      let braceCount = 1 // Start with 1 to account for the opening brace
+      bodyEndIndex = bodyStartIndex + 1 // Move past the opening brace
+
+      while (bodyEndIndex < content.length && braceCount > 0) {
+        // Traverse the content to find the closing brace
+        if (content[bodyEndIndex] === '{') {
+          braceCount++
+        }
+        else if (content[bodyEndIndex] === '}') {
+          braceCount--
+        }
+        bodyEndIndex++ // Move to the next character
+      }
 
-  while (endIndex < content.length && openBraces > 0) {
-    const char = content[endIndex]
+      // Extract the function body, excluding the final closing brace
+      body = content.slice(bodyStartIndex + 1, bodyEndIndex - 1).trim()
+    }
+    else {
+      // If the function body does not start with '{', it's a concise body arrow function
+      while (bodyEndIndex < content.length && content[bodyEndIndex] !== ';') {
+        // Traverse until we find the end of the concise body (usually a semicolon)
+        bodyEndIndex++
+      }
 
-    if (char === '{') {
-      openBraces++
+      // Extract the function body up to the semicolon or end of content
+      body = content.slice(bodyStartIndex, bodyEndIndex).trim()
     }
-    if (char === '}') {
-      openBraces--
+
+    return {
+      name,
+      body,
+      end: bodyEndIndex, // Position after the end of the function body
     }
-    body += char
-    endIndex++
   }
-
-  return { body, end: endIndex }
+  else {
+    return null
+  }
 }
 
+// Cleans the function name by removing any leading 'const' keyword.
 function cleanFunctionName(funcName: string): string {
   return funcName.replace(/^const\s*/, '')
 }
@@ -123,31 +144,31 @@ const checkFunctionSize = (script: SFCScriptBlock | null, filePath: string) => {
     let funcBody = ''
     let isFunction = false
 
-    // Search for a function declaration or arrow function
+    // Check for function declarations
     if (content.slice(index, index + FUNCTION_KEYWORD_LENGTH) === 'function') {
-      index += FUNCTION_KEYWORD_LENGTH
-      isFunction = true
-      funcName = parseFunctionName(content, index)
-      index = skipToFunctionBody(content, index)
+      const regularFunctionInfo = parseRegularFunction(content, index)
+      if (regularFunctionInfo) {
+        isFunction = true
+        funcName = regularFunctionInfo.name
+        funcBody = regularFunctionInfo.body
+        index = regularFunctionInfo.end
+      }
     }
-
-    if (content.slice(index, index + CONST_KEYWORD_LENGTH) === 'const') {
-      // TODO this will find the first const keyword, but it could be a variable declaration see #174
+    if (content.slice(index, index + CONST_KEYWORD_LENGTH) === 'const') { // Check for arrow functions
       const arrowFunctionInfo = parseArrowFunction(content, index)
       if (arrowFunctionInfo) {
         isFunction = true
         funcName = arrowFunctionInfo.name
-        index = arrowFunctionInfo.bodyStart
+        funcBody = arrowFunctionInfo.body
+        index = arrowFunctionInfo.end // move the index past the end of the function
       }
     }
 
     if (isFunction) {
-      const { body, end } = extractFunctionBody(content, index)
-      funcBody = body
-      index = end
-      addFunctionToFiles(funcName, funcBody, filePath)
+      const lineNumber = getLineNumber(content.trim(), funcName)
+      addFunctionToFiles({ funcName, funcBody, lineNumber, filePath })
     }
-    if (!isFunction) {
+    else {
       index++
     }
   }


Test patch:
diff --git a/src/rules/rrd/functionSize.test.ts b/src/rules/rrd/functionSize.test.ts
index cf4a8f4..ce7db70 100644
--- a/src/rules/rrd/functionSize.test.ts
+++ b/src/rules/rrd/functionSize.test.ts
@@ -19,7 +19,7 @@ describe('checkFunctionSize', () => {
             }
 
             return {
-              message: msg.trim() 
+              message: msg.trim()
             }
           }
         </script>
@@ -54,6 +54,22 @@ describe('checkFunctionSize', () => {
     expect(reportFunctionSize()).toStrictEqual([])
   })
 
+  it('should not report files with multiple short functions', () => {
+    const script = {
+      content: `
+      <script setup>
+        function shortFunctionOne() { console.log('Short function one'); }
+        function shortFunctionTwo() { console.log('Short function two'); }
+        const shortArrowFunction = () => console.log('Short arrow function');
+      </script>
+    `,
+    } as SFCScriptBlock
+    const fileName = 'multiple-short-functions.vue'
+    checkFunctionSize(script, fileName)
+    expect(reportFunctionSize().length).toBe(0)
+    expect(reportFunctionSize()).toStrictEqual([])
+  })
+
   it('should report files with one function exceeding the limit', () => {
     const script = {
       content: `
@@ -98,7 +114,7 @@ describe('checkFunctionSize', () => {
       file: fileName,
       rule: `${TEXT_INFO}rrd ~ function size${TEXT_RESET}`,
       description: `👉 ${TEXT_WARN}Functions must be shorter than ${MAX_FUNCTION_LENGTH} lines.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/function-size.html`,
-      message: `function ${BG_WARN}(${funcName})${BG_RESET} is too long: ${BG_WARN}25 lines${BG_RESET} 🚨`,
+      message: `function ${BG_WARN}(${funcName}#2)${BG_RESET} is too long: ${BG_WARN}23 lines${BG_RESET} 🚨`,
     }])
   })
 
@@ -173,12 +189,12 @@ describe('checkFunctionSize', () => {
       file: fileName,
       rule: `${TEXT_INFO}rrd ~ function size${TEXT_RESET}`,
       description: `👉 ${TEXT_WARN}Functions must be shorter than ${MAX_FUNCTION_LENGTH} lines.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/function-size.html`,
-      message: `function ${BG_WARN}(dummyRegularFunction)${BG_RESET} is too long: ${BG_WARN}31 lines${BG_RESET} 🚨`,
+      message: `function ${BG_WARN}(dummyRegularFunction#2)${BG_RESET} is too long: ${BG_WARN}29 lines${BG_RESET} 🚨`,
     }, {
       file: fileName,
       rule: `${TEXT_INFO}rrd ~ function size${TEXT_RESET}`,
       description: `👉 ${TEXT_WARN}Functions must be shorter than ${MAX_FUNCTION_LENGTH} lines.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/function-size.html`,
-      message: `function ${BG_WARN}(dummyArrowFunction)${BG_RESET} is too long: ${BG_WARN}27 lines${BG_RESET} 🚨`,
+      message: `function ${BG_WARN}(dummyArrowFunction#34)${BG_RESET} is too long: ${BG_WARN}23 lines${BG_RESET} 🚨`,
     }])
   })
 
@@ -237,11 +253,11 @@ describe('checkFunctionSize', () => {
       file: fileName,
       rule: `${TEXT_INFO}rrd ~ function size${TEXT_RESET}`,
       description: `👉 ${TEXT_WARN}Functions must be shorter than ${MAX_FUNCTION_LENGTH} lines.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/function-size.html`,
-      message: `function ${BG_ERR}(getOpenBookings)${BG_RESET} is too long: ${BG_ERR}43 lines${BG_RESET} 🚨`,
+      message: `function ${BG_ERR}(getOpenBookings#2)${BG_RESET} is too long: ${BG_ERR}41 lines${BG_RESET} 🚨`,
     }])
   })
 
-  it.todo('should report files with correct function name', () => {
+  it('should report files with correct function name', () => {
     const script = {
       content: `
         import inquirer from 'inquirer';  // it cause some error when this file is ts
@@ -279,6 +295,11 @@ describe('checkFunctionSize', () => {
     const fileName = 'arrow-function-size.vue'
     checkFunctionSize(script, fileName)
     expect(reportFunctionSize().length).toBe(1)
-    expect(reportFunctionSize()[0].message).toContain(`function ${BG_WARN}(createFiles)${BG_RESET} is too long: ${BG_WARN}22 lines${BG_RESET} 🚨`)
+    expect(reportFunctionSize()).toStrictEqual([{
+      file: fileName,
+      rule: `${TEXT_INFO}rrd ~ function size${TEXT_RESET}`,
+      description: `👉 ${TEXT_WARN}Functions must be shorter than ${MAX_FUNCTION_LENGTH} lines.${TEXT_RESET} See: https://vue-mess-detector.webmania.cc/rules/rrd/function-size.html`,
+      message: `function ${BG_WARN}(createFiles#10)${BG_RESET} is too long: ${BG_WARN}20 lines${BG_RESET} 🚨`,
+    }])
   })
 })