From 5f085a740d455930fa32fc7f20dc54763eba4585 Mon Sep 17 00:00:00 2001 From: David-Pena Date: Sun, 21 Jul 2024 08:29:13 -0700 Subject: [PATCH] feat: add vue-caution implicit-parent-child-communication rule --- src/analyzer.ts | 4 + .../implicitParentChildCommunication.test.ts | 146 ++++++++++++++++++ .../implicitParentChildCommunication.ts | 55 +++++++ 3 files changed, 205 insertions(+) create mode 100644 src/rules/vue-caution/implicitParentChildCommunication.test.ts create mode 100644 src/rules/vue-caution/implicitParentChildCommunication.ts diff --git a/src/analyzer.ts b/src/analyzer.ts index 897ce250..1a81d4d1 100644 --- a/src/analyzer.ts +++ b/src/analyzer.ts @@ -22,6 +22,7 @@ import { checkFunctionSize, reportFunctionSize } from './rules/rrd/functionSize' import { checkParameterCount, reportParameterCount } from './rules/rrd/parameterCount' import { checkShortVariableName, reportShortVariableName } from './rules/rrd/shortVariableName' import { checkSimpleComputed, reportSimpleComputed } from './rules/vue-strong/simpleComputed' +import { checkImplicitParentChildCommunication, reportImplicitParentChildCommunication } from './rules/vue-caution/implicitParentChildCommunication' let filesCount = 0 @@ -90,6 +91,7 @@ export const analyze = (dir: string) => { checkParameterCount(script, filePath) checkShortVariableName(script, filePath) checkSimpleComputed(script, filePath) + checkImplicitParentChildCommunication(script, filePath) } descriptor.styles.forEach(style => { @@ -126,7 +128,9 @@ export const analyze = (dir: string) => { errors += reportSimpleComputed() // vue-reccomended rules + // vue-caution rules + errors += reportImplicitParentChildCommunication() // rrd rules errors += reportScriptLength() diff --git a/src/rules/vue-caution/implicitParentChildCommunication.test.ts b/src/rules/vue-caution/implicitParentChildCommunication.test.ts new file mode 100644 index 00000000..6b07c4d4 --- /dev/null +++ b/src/rules/vue-caution/implicitParentChildCommunication.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it, vi } from 'vitest'; +import { SFCScriptBlock } from '@vue/compiler-sfc'; +import { BG_ERR, BG_RESET, BG_WARN } from '../asceeCodes' +import { checkImplicitParentChildCommunication, reportImplicitParentChildCommunication } from './implicitParentChildCommunication'; + +const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + +describe('checkImplicitParentChildCommunication', () => { + it('should not report files where there is no implicit parent-child communication', () => { + const script = { + content: ` + + + + ` + } as SFCScriptBlock; + const filename = 'no-implicit-pcc.vue' + checkImplicitParentChildCommunication(script, filename) + expect(reportImplicitParentChildCommunication()).toBe(0) + expect(mockConsoleLog).not.toHaveBeenCalled() + }) + + it('should report files where there is a prop mutation', () => { + const script = { + content: ` + + + + ` + } as SFCScriptBlock; + const filename = 'props-mutation.vue' + const lineNumber = 7 + checkImplicitParentChildCommunication(script, filename) + expect(reportImplicitParentChildCommunication()).toBe(1) + expect(mockConsoleLog).toHaveBeenCalled() + expect(mockConsoleLog).toHaveBeenLastCalledWith( + `- ${filename}#${lineNumber} ${BG_WARN}(todo)${BG_RESET} 🚨` + ) + }) + + it('should report files where $parent/getCurrentInstance is present', () => { + const script = { + content: ` + + + + ` + } as SFCScriptBlock; + const filename = "parent-instance.vue" + const lineNumber = 2 + checkImplicitParentChildCommunication(script, filename) + expect(reportImplicitParentChildCommunication()).toBe(2) + expect(mockConsoleLog).toHaveBeenCalled() + expect(mockConsoleLog).toHaveBeenLastCalledWith( + `- ${filename}#${lineNumber} ${BG_WARN}(getCurrentInstance)${BG_RESET} 🚨` + ) + }) + + it('should report files where props mutation & $parent/getCurrentInstance are present', () => { + const script = { + content: ` + + + + ` + } as SFCScriptBlock; + const filename = "complex-sample.vue" + const lineNumber = 2 + checkImplicitParentChildCommunication(script, filename) + expect(reportImplicitParentChildCommunication()).toBe(4) + expect(mockConsoleLog).toHaveBeenCalled() + expect(mockConsoleLog).toHaveBeenLastCalledWith( + `- ${filename}#${lineNumber} ${BG_WARN}(getCurrentInstance)${BG_RESET} 🚨` + ) + }) +}) \ No newline at end of file diff --git a/src/rules/vue-caution/implicitParentChildCommunication.ts b/src/rules/vue-caution/implicitParentChildCommunication.ts new file mode 100644 index 00000000..c518893f --- /dev/null +++ b/src/rules/vue-caution/implicitParentChildCommunication.ts @@ -0,0 +1,55 @@ +import { SFCScriptBlock } from '@vue/compiler-sfc'; +import { BG_RESET, BG_WARN, TEXT_WARN, TEXT_RESET, TEXT_INFO } from '../asceeCodes'; +import getLineNumber from '../getLineNumber'; +import { getUniqueFilenameCount } from '../../helpers'; + +type ImplicitParentChildCommunicationFile = { filename: string, message: string }; + +const implicitParentChildCommunicationFiles: ImplicitParentChildCommunicationFile[] = []; + +const checkImplicitParentChildCommunication = (script: SFCScriptBlock, filePath: string) => { + const propsRegex = /defineProps\(([^)]+)\)/; + const vModelRegex = /v-model\s*=\s*"([^"]+)"/; + const parentRegex = /\$parent|getCurrentInstance/g; + + // Extract defined props + const propsMatch = script.content.match(propsRegex); + + // Check for props mutation + const vModelMatch = script.content.match(vModelRegex); + if (vModelMatch) { + const vModelProp = vModelMatch[1].split('.')[0]; + const definedProps = propsMatch ? propsMatch[1] : ''; + + // Check if matched prop is inside `v-model` directive + if (definedProps.includes(vModelProp)) { + const lineNumber = getLineNumber(script.content.trim(), definedProps); + implicitParentChildCommunicationFiles.push({ filename: filePath, message: `${filePath}#${lineNumber} ${BG_WARN}(${vModelProp})${BG_RESET}` }) + } + } + + // Check for $parent / getCurrentInstance + const parentMatch = script.content.match(parentRegex); + if (parentMatch) { + const lineNumber = getLineNumber(script.content.trim(), parentMatch[0]); + implicitParentChildCommunicationFiles.push({ filename: filePath, message: `${filePath}#${lineNumber} ${BG_WARN}(${parentMatch[0]})${BG_RESET}` }) + } +} + +const reportImplicitParentChildCommunication = () => { + if (implicitParentChildCommunicationFiles.length > 0) { + // Count only non duplicated objects (by its `filename` property) + const fileCount = getUniqueFilenameCount(implicitParentChildCommunicationFiles, 'filename'); + + console.log( + `\n${TEXT_INFO}vue-caution${TEXT_RESET} ${BG_WARN}implicit parent-child communication${BG_RESET} detected in ${fileCount} files.` + ) + console.log(`👉 ${TEXT_WARN}Avoid implicit parent-child communication to maintain clear and predictable component behavior.${TEXT_RESET} See: https://vuejs.org/style-guide/rules-use-with-caution.html#implicit-parent-child-communication`) + implicitParentChildCommunicationFiles.forEach(file => { + console.log(`- ${file.message} 🚨`) + }) + } + return implicitParentChildCommunicationFiles.length; +} + +export { checkImplicitParentChildCommunication, reportImplicitParentChildCommunication }; \ No newline at end of file