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

feat: add vue-caution implicit-parent-child-communication rule #67

Merged
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
4 changes: 4 additions & 0 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -126,7 +128,9 @@ export const analyze = (dir: string) => {
errors += reportSimpleComputed()

// vue-reccomended rules

// vue-caution rules
errors += reportImplicitParentChildCommunication()

// rrd rules
errors += reportScriptLength()
Expand Down
146 changes: 146 additions & 0 deletions src/rules/vue-caution/implicitParentChildCommunication.test.ts
Original file line number Diff line number Diff line change
@@ -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: `
<script setup>
defineProps({
todo: {
type: Object,
required: true
}
})

const emit = defineEmits(['input'])
</script>

<template>
<input :value="todo.text" @input="emit('input', $event.target.value)" />
</template>
`
} 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: `
<script setup>
defineProps({
todo: {
type: Object,
required: true
}
})
</script>

<template>
<input v-model="todo.text" />
</template>
`
} 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: `
<script setup>
import { getCurrentInstance } from 'vue'

const props = defineProps({
todo: {
type: Object,
required: true
}
})

const instance = getCurrentInstance()

function removeTodo() {
const parent = instance.parent
if (!parent) return

parent.props.todos = parent.props.todos.filter((todo) => {
return todo.id !== props.todo.id
})
}
</script>

<template>
<span>
{{ todo.text }}
<button @click="removeTodo">×</button>
</span>
</template>
`
} 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: `
<script setup>
import { getCurrentInstance } from 'vue'

const props = defineProps({
todo: {
type: Object,
required: true
}
})

const instance = getCurrentInstance()

function removeTodo() {
const parent = instance.parent
if (!parent) return

parent.props.todos = parent.props.todos.filter((todo) => {
return todo.id !== props.todo.id
})
}
</script>

<template>
<span>
{{ todo.text }}
<input v-model="todo.text" />
</span>
</template>
`
} 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} 🚨`
)
})
})
55 changes: 55 additions & 0 deletions src/rules/vue-caution/implicitParentChildCommunication.ts
Original file line number Diff line number Diff line change
@@ -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<ImplicitParentChildCommunicationFile>(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 };
Loading