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

Issue:
`functionSize` check is extremely slow
**Describe the bug**
When we run `yarn analyze` with the `functionSize` check for certain vue files it become extremely slow (5s) and could cause total freezing #116 

**To Reproduce**
Run `yarn analyze` on a codebase what contains this file: https://github.com/rrd108/sadhana/blob/main/src/components/SettingsNotifications.vue
Currently the arrow function check is commented out, so you should uncomment, and build before you run analyze.

**Expected behavior**
All checks should run under 1 sec

**Additional context**
The issue is somehow connected the arrow function regex. I hope @David-Pena you can find out why it happens and how to fix it.


Pull request description:
refactor: checkFunctionSize new logic
fixes #119 

Patch:
diff --git a/src/rules/rrd/functionSize.ts b/src/rules/rrd/functionSize.ts
index 2416fff..e7f0fd1 100644
--- a/src/rules/rrd/functionSize.ts
+++ b/src/rules/rrd/functionSize.ts
@@ -6,39 +6,139 @@ const results: FileCheckResult[] = []
 
 export const MAX_FUNCTION_LENGTH = 20 // completely rrd made-up number
 
-const addFunctionToFiles = (match: RegExpExecArray, filePath: string) => {
-  const funcName = match[1]
-  const funcBody = match[2]
-
-  // Check if the function block has more than `MAX_FUNCTION_LENGTH` lines
+function addFunctionToFiles(funcName: string, funcBody: string, filePath: string) {
   const lineCount = funcBody.split('\n').length
   if (lineCount > MAX_FUNCTION_LENGTH) {
-    results.push({ filePath, message: `function ${BG_ERR}(${funcName})${BG_RESET} is too long` })
+    results.push({ filePath, message: `function ${BG_ERR}(${cleanFunctionName(funcName)})${BG_RESET} is too long` })
+  }
+}
+
+// Helper to parse the function name after the 'function' keyword
+function parseFunctionName(content: string, startIndex: number): string {
+  let name = ''
+  let i = startIndex
+
+  // Skip whitespace and move to the function name
+  while (i < content.length && /\s/.test(content[i])) {
+    i++
+  }
+
+  // Capture the function name and skip the 'const' keyword if it's an arrow function
+  if (content.slice(i, i + 5) === 'const') {
+    i += 5
+    // Skip any whitespace after 'const'
+    while (i < content.length && /\s/.test(content[i])) {
+      i++
+    }
+  }
+
+  // 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 '{'
+}
+
+// Helper to parse arrow function
+function parseArrowFunction(content: string, startIndex: number) {
+  let name = ''
+  let bodyStart = -1
+
+  // Find variable name
+  while (startIndex < content.length && content[startIndex] !== '=') {
+    if (/\w/.test(content[startIndex])) {
+      name += content[startIndex]
+    }
+    startIndex++
+  }
+
+  // Skip arrow syntax (=>)
+  startIndex = content.indexOf('=>', startIndex)
+  if (startIndex === -1)
+    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
+
+  while (endIndex < content.length && openBraces > 0) {
+    const char = content[endIndex]
+
+    if (char === '{')
+      openBraces++
+    if (char === '}')
+      openBraces--
+    body += char
+    endIndex++
+  }
+
+  return { body, end: endIndex }
+}
+
+function cleanFunctionName(funcName: string): string {
+  return funcName.replace(/^const\s*/, '')
 }
 
 const checkFunctionSize = (script: SFCScriptBlock | null, filePath: string) => {
   if (!script) {
     return
   }
-  const regexNormalFunction
-    = /function\s+([\w$]+)\s*\([^)]*\)\s*\{([^{}]*(([^{}]*\{[^{}]*\}[^{}]*)*[^{}]*))\}/g
-  const regexArrowFunction
-    = /const\s+([\w$]+)\s*=\s*\([^)]*\)\s*=>\s*\{([^{}]*(([^{}]*\{[^{}]*\}[^{}]*)*[^{}]*))\}/g
-  // TODO it does not match arrow functions with no curly braces
 
-  let match
+  const content = script.content
+  const length = content.length
+  let index = 0
 
-  // eslint-disable-next-line no-cond-assign
-  while ((match = regexNormalFunction.exec(script.content)) !== null) {
-    addFunctionToFiles(match, filePath)
-  }
+  while (index < length) {
+    let funcName = ''
+    let funcBody = ''
+    let isFunction = false
 
-  // TODO temporary switch off see #116
+    // Search for a function declaration or arrow function
+    if (content.slice(index, index + 8) === 'function') {
+      index += 8
+      isFunction = true
+      funcName = parseFunctionName(content, index)
+      index = skipToFunctionBody(content, index)
+    }
+    else if (content.slice(index, index + 5) === 'const') {
+      const arrowFunctionInfo = parseArrowFunction(content, index)
+      if (arrowFunctionInfo) {
+        isFunction = true
+        funcName = arrowFunctionInfo.name
+        index = arrowFunctionInfo.bodyStart
+      }
+    }
 
-  // while ((match = regexArrowFunction.exec(script.content)) !== null) {
-  //  addFunctionToFiles(match, filePath)
-  // }
+    if (isFunction) {
+      const { body, end } = extractFunctionBody(content, index)
+      funcBody = body
+      index = end
+      addFunctionToFiles(funcName, funcBody, filePath)
+    }
+    else {
+      index++
+    }
+  }
 }
 
 const reportFunctionSize = () => {


Test patch:
diff --git a/src/rules/rrd/functionSize.test.ts b/src/rules/rrd/functionSize.test.ts
index 098bf5b..86fb84b 100644
--- a/src/rules/rrd/functionSize.test.ts
+++ b/src/rules/rrd/functionSize.test.ts
@@ -31,6 +31,29 @@ describe('checkFunctionSize', () => {
     expect(reportFunctionSize()).toStrictEqual([])
   })
 
+  it('should not report files where arrow functions without curly braces do not exceed the recommended limit', () => {
+    const script = {
+      content: `
+        <script setup>
+          const getOpenBookings = (page: number) =>
+            axios
+              .get(\`\${import.meta.env.VITE_APP_API_URL}bookings/listOpen.json?page=\${page}\`, store.tokenHeader)
+              .then(res => {
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+              })
+              .catch(err => console.error(err))
+
+          const shortFunction = (a, b) => a + b
+        </script>
+      `,
+    } as SFCScriptBlock
+    const fileName = 'arrow-function-size.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: `
@@ -145,19 +168,67 @@ describe('checkFunctionSize', () => {
     } as SFCScriptBlock
     const fileName = 'function-size.vue'
     checkFunctionSize(script, fileName)
-    //  expect(reportFunctionSize().length).toBe(2) TODO temporary disabled for #116
-    expect(reportFunctionSize().length).toBe(1)
+    expect(reportFunctionSize().length).toBe(2)
     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_ERR}(dummyRegularFunction)${BG_RESET} is too long 🚨`,
-    }, /* TODO temporary disabled for #116
-    , {
+    }, {
+      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}(dummyArrowFunction)${BG_RESET} is too long 🚨`,
+    }])
+  })
+
+  it('should report files with one arrow function without curly braces exceeding the limit', () => {
+    const script = {
+      content: `
+        <script setup>
+          const getOpenBookings = (page: number) =>
+            axios
+              .get(\`\${import.meta.env.VITE_APP_API_URL}bookings/listOpen.json?page=\${page}\`, store.tokenHeader)
+              .then(res => {
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+                bookings.value = res.data.bookings
+                paginate.value = res.data.paginate
+              })
+              .catch(err => console.error(err))
+
+          const shortFunction = (a, b) => a + b
+        </script>
+      `,
+    } as SFCScriptBlock
+    const fileName = 'arrow-function-size.vue'
+    checkFunctionSize(script, fileName)
+    expect(reportFunctionSize().length).toBe(1)
+    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_ERR}(dummyArrowFunction)${BG_RESET} 🚨`,
-    } */])
+      message: `function ${BG_ERR}(getOpenBookings)${BG_RESET} is too long 🚨`,
+    }])
   })
 })