Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: checkFunctionSize new logic #129

Merged
merged 1 commit into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 77 additions & 6 deletions src/rules/rrd/functionSize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
Expand Down Expand Up @@ -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 🚨`,
}])
})
})
140 changes: 120 additions & 20 deletions src/rules/rrd/functionSize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
Loading